PyTorch:模型训练和预测

Posted -柚子皮-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PyTorch:模型训练和预测相关的知识,希望对你有一定的参考价值。

-柚子皮-

模型训练和预测

模型训练

单机训练

传统的batch训练函数

简单的说就是进来一个batch的数据,计算一次梯度,更新一次网络。

for i,(images,target) in enumerate(train_loader):
    # 1. input output
    images = images.cuda(non_blocking=True)
    target = torch.from_numpy(np.array(target)).float().cuda(non_blocking=True)
    outputs = model(images)
    loss = criterion(outputs,target)

    # 2. backward
    optimizer.zero_grad()   # reset gradient
    loss.backward()
    optimizer.step()            

1 获取loss:输入图像和标签,通过infer计算得到预测值,计算损失函数;
2 optimizer.zero_grad() 清空过往梯度;  lz:这里需要手动进行optimizer.zero_grad(),不然pytorch中默认是累加的。
3 loss.backward() 反向传播,计算当前梯度;
4 optimizer.step() 根据梯度更新网络参数

使用梯度累加的batch训练函数

梯度累加就是,每次获取1个batch的数据,计算1次梯度,梯度不清空,不断累加,累加一定次数后,根据累加的梯度更新网络参数,然后清空梯度,进行下一次循环。

for i,(images,target) in enumerate(train_loader):
    # 1. input output
    images = images.cuda(non_blocking=True)
    target = torch.from_numpy(np.array(target)).float().cuda(non_blocking=True)
    outputs = model(images)
    loss = criterion(outputs,target)

    # 2.1 loss regularization
    loss = loss/accumulation_steps   
    # 2.2 back propagation
    loss.backward()
    # 3. update parameters of net
    if((i+1)%accumulation_steps)==0:
        # optimizer the net
        optimizer.step()        # update parameters of net
        optimizer.zero_grad()   # reset gradient

1 获取loss:输入图像和标签,通过infer计算得到预测值,计算损失函数;
2 loss.backward() 反向传播,计算当前梯度;   lz:2和3之间应该有一个pytorch自动累加梯度的过程(应该就是backward里面的过程吧)。
3 多次循环步骤1-2,不清空梯度,使梯度累加在已有梯度上;
4 梯度累加了一定次数后,先optimizer.step() 根据累计的梯度更新网络参数,然后optimizer.zero_grad() 清空过往梯度,为下一波梯度累加做准备;

与非梯度累加区别

      一定条件下,batchsize越大训练效果越好,梯度累加则实现了batchsize的变相扩大,如果accumulation_steps为8,则batchsize '变相' 扩大了8倍,是解决显存受限的一个不错的trick。

使用时需要注意,学习率也要适当放大。经验法则是,如果batch size加倍,那么学习率就加倍

PyTorch中在反向传播前为什么要手动将梯度清零?

有时候会不会觉得optimizer.zero_grad()这句很多余,怎么不自动清零,需要手动清零呢?原因在于在PyTorch中,计算得到的梯度值会自动进行累加(而不是覆盖)。pytorch选择自动累加而不是覆盖的原因:

1 从内存消耗的角度来看。这种模式可以让梯度玩出更多花样,比如说上面讲到的梯度累加(gradient accumulation)实现的“显存受限”解决:在内存大小不够的情况下叠加多个batch的grad作为一个大batch进行迭代,因为二者得到的梯度是等价的综上可知,这种梯度累加的思路是对内存的极大友好,是由FAIR的设计理念出发的。即当你GPU显存较少时,你又想要调大batch-size,此时你就可以利用PyTorch的这个性质进行梯度的累加来进行backward。

示例:

在你已经达到计算资源上限的情况下,你的batch size仍然太小(比如8),然后我们需要模拟一个更大的batch size来进行梯度下降,以提供一个良好的估计。

假设我们想要达到128的batch size大小。我们需要以batch size为8执行16个前向传播和向后传播,然后再执行一次优化步骤。

# clear last step
optimizer.zero_grad()

# 16 accumulated gradient steps
scaled_loss = 0
for accumulated_step_i in range(16):
     out = model.forward()
     loss = some_loss(out,y)    
     loss.backward()
      scaled_loss += loss.item()
      
# update weights after 8 steps. effective batch = 8*16
optimizer.step()

# loss is now scaled up by the number of accumulated batches
actual_loss = scaled_loss / 16

2 利用梯度累加,可以在最多保存一张计算图的情况下进行multi-task任务的训练:

从PyTorch的设计原理上来说,在每次进行前向计算得到pred时,会产生一个用于梯度回传的计算图,这张图储存了进行back propagation需要的中间结果,当调用了.backward()后,会从内存中将这张图进行释放。

for idx, data in enumerate(train_loader):
    xs, ys = data
    optmizer.zero_grad()
    # 计算d(l1)/d(x)
    pred1 = model1(xs) #生成graph1
    loss1 = loss_fn1(pred1, ys)
    loss1.backward()  #释放graph1

    # 计算d(l2)/d(x)
    pred2 = model2(xs)#生成graph2
    loss2 = loss_fn2(pred2, ys)
    loss2.backward()  #释放graph2

    # 使用d(l1)/d(x)+d(l2)/d(x)进行优化
    optmizer.step()

[PyTorch中在反向传播前为什么要手动将梯度清零?]

模型训练加速

16-bit 精度

16bit精度是将内存占用减半的惊人技术。大多数模型使用32bit精度数字进行训练。然而,最近的研究发现,16bit模型也可以工作得很好。混合精度意味着对某些内容使用16bit,但将权重等内容保持在32bit。

要在Pytorch中使用16bit精度,请安装NVIDIA的apex库,并对你的模型进行这些更改。

# enable 16-bit on the model and the optimizer
model, optimizers = amp.initialize(model, optimizers, opt_level='O2')

# when doing .backward, let amp do it so it can scale the loss
with amp.scale_loss(loss, optimizer) as scaled_loss:                      
    scaled_loss.backward()

amp包会处理好大部分事情。如果梯度爆炸或趋向于0,它甚至会缩放loss。

在lightning中,启用16bit并不需要修改模型中的任何内容,也不需要执行我上面所写的操作。设置Trainer(precision=16)就可以了。

trainer = Trainer(amp_level='O2', use_amp=False)
trainer.fit(model)

移动到多个GPUs中

多节点GPU训练

在单个节点上多GPU更快的训练distributedDataParallel

[9个技巧让你的PyTorch模型训练变得飞快!]

Gpu训练

要进行2个张量的运算,就必须都在CPU或者都在GPU上。对于不同存储位置的变量,我们是不可以对他们直接进行计算的。存储在不同位置中的数据是不可以直接进行交互计算的。否则出错:RuntimeError: Expected object of backend CUDA but got backend CPU for ***。[PyTorch 20.GPU训练]

把数据和模型从cpu移到gpu中的两种方法

use_cuda = torch.cuda.is_available()

# 方法一:
if use_cuda:
    data = data.cuda()
    model.cuda()

# 方法二:
device = torch.device("cuda" if use_cuda else "cpu")
data = data.to(device)
model.to(device)
个人比较习惯第二种方法,可以少一个 if 语句。而且该方法还可以通过设备号指定使用哪个GPU设备,比如使用0号设备:

device = torch.device("cuda:0" if use_cuda else "cpu")

[PyTorch:tensor-基本操作---CUDA 的用法]

1 如果模型已经在GPU上了,model.to(device)/model.cuda()不会做任何事情。

2 对于自己创建的模型类,由于继承了 torch.nn.Module ,则可同样使用 model.to(device)/model.cuda() 来将模型中用到的所有参数都存储到显存中去。

model_cuda = model.cuda()后,将 cpu类型数据 投入 model_cuda 中去可以发现,系统会报错,而将 .cuda 之后的gpu数据投入 model_cuda 才能正常工作,并且输出的也是具有cuda的数据类型。

self.dnn_layer = MLP(input_dim, dnn_hidden_units, activation=dnn_activation,
                    use_bn=dnn_use_bn, l2_reg=l2_reg_dnn, dropout_rate=dnn_dropout,
                    init_std=init_std, device=device)
self.to(device)
...
print("self.dnn_layer.device: ", self.dnn_layer.device) #cpu,模型to('gpu')时函数本身没有变化,只是参数put到gpu了。
print("self.dnn_layer.parameters()).device: ", next(self.dnn_layer.parameters()).device) #gpu

3 当我们对模型存储到显存中去之后,那么这个模型中的方法后面所创建出来的Tensor是不是都会默认变成cuda的数据类型?答案是否定的。

[浅谈将Pytorch模型从CPU转换成GPU]

Note: 

def compile(self, optimizer_name, loss_func_name=None, metrics_name=None):
    """
    :param optimizer_name: String (name of optimizer) or optimizer instance. See [optimizers](https://pytorch.org/docs/stable/optim.html).
    :param loss_func_name: String (name of objective function) or objective function. See [losses](https://pytorch.org/docs/stable/nn.functional.html#loss-functions).
    :param metrics_name: List of metrics to be evaluated by the model during training and testing. Typically you will use `metrics=['accuracy']`.
    """
    self.optimizer = self._get_optim(optimizer_name)
    self.loss_func = self._get_loss_func(loss_func_name)
    self.metrics = self._get_metrics(metrics_name)

def _get_optim(self, optimizer):
    if isinstance(optimizer, str):
        if optimizer == "sgd":
            optim = torch.optim.SGD(self.parameters(), lr=0.01)
        elif optimizer == "adam":
            optim = torch.optim.Adam(self.parameters())
        elif optimizer == "adagrad":
            optim = torch.optim.Adagrad(self.parameters())
        elif optimizer == "rmsprop":
            optim = torch.optim.RMSprop(self.parameters())
        else:
            raise NotImplementedError
    else:
        optim = optimizer
    return optim

def optimize(self, max_norm=None, norm_type=2):
    '''
    进行最优化操作
    '''
    with time_block("backward_in_optimize"):
        self.loss.backward()
    if max_norm:
        torch.nn.utils.clip_grad_norm_(self.parameters(), max_norm, norm_type)
    print("next(self.parameters()).device: ", next(self.parameters()).device)
    self.optimizer.step()

init中初始化时:

self.to(device)

self.compile(optimizer, loss, metrics) #需要在self.to(device)后面,因为optim = torch.optim.Adagrad(self.parameters())中next(self.parameters()).device的类型还未确定,self.parameters()为gpu时,optim才是gpu中,self.optimizer.step()执行时才不会出错:RuntimeError: Expected object of backend CPU but got backend CUDA for argument #4 'tensor1'。

指定GPU编号

设置当前使用的GPU设备仅为0号设备,设备名称为 /gpu:0:

os.environ["CUDA_VISIBLE_DEVICES"] = "0"

设置当前使用的GPU设备为0,1号两个设备,名称依次为 /gpu:0、/gpu:1:

os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"

根据顺序表示优先使用0号设备,然后使用1号设备。

Note: 指定GPU的命令需要放在和神经网络相关的一系列操作的前面。

单机多GPU的实现代码

[Pytorch 高效使用GPU的操作]

分布式训练

[PyTorch:模型训练-分布式训练]

 

模型预测

model.eval()作用:不启用 BatchNormalization 和 Dropout,保证BN和dropout不发生变化,pytorch框架会自动把BN和Dropout固定住,不会取平均,而是用训练好的值,不然的话,一旦test的batch_size过小,很容易就会被BN层影响结果。

# Set to not-training mode to disable dropout
model.eval()     #实际调用model.train(False) 

...

with torch.no_grad():    #禁止梯度的计算

    y_pred = model(x)

    loss = ...

训练时:

# Set back to training mode
model.train(True)  # todo

 

 

训练设置

梯度裁剪

nn.utils.clip_grad_norm_ 的参数
parameters – 一个基于变量的迭代器,会进行梯度归一化
max_norm – 梯度的最大范数
norm_type – 规定范数的类型,默认为L2
Note: 梯度裁剪在某些任务上会额外消耗大量的计算时间。

[https://pytorch.org/docs/master/generated/torch.nn.utils.clip_grad_norm_.html]

示例

def optimize(self, max_norm=None, norm_type=2):
        '''
        进行最优化操作
        '''
        self.loss.backward()
        if max_norm:
            torch.nn.utils.clip_grad_norm_(self.parameters(), max_norm, norm_type)
        self.optimizer.step()

import torch.nn as nn

outputs = model(data)
loss= loss_fn(outputs, target)
optimizer.zero_grad()

self.optimize()

16-bit 精度

16bit精度是将内存占用减半的惊人技术。大多数模型使用32bit精度数字进行训练。然而,最近的研究发现,16bit模型也可以工作得很好。混合精度意味着对某些内容使用16bit,但将权重等内容保持在32bit。

要在Pytorch中使用16bit精度,请安装NVIDIA的apex库,并对你的模型进行这些更改。

# enable 16-bit on the model and the optimizer
model, optimizers = amp.initialize(model, optimizers, opt_level='O2')

# when doing .backward, let amp do it so it can scale the loss
with amp.scale_loss(loss, optimizer) as scaled_loss:                      
    scaled_loss.backward()

amp包会处理好大部分事情。如果梯度爆炸或趋向于0,它甚至会缩放loss。

防止验证模型时爆显存

torch.no_grad()

torch.no_grad()是一个上下文管理器,用来禁止梯度的计算,通常用来网络推断中(因为验证模型时不需要求导,即不需要梯度计算),关闭autograd可以减少计算内存的使用量,提高速度,如果不关闭可能会爆显存。
with torch.no_grad():
    # 使用model进行预测的代码
    pass

示例
>>> a = torch.tensor([1.0, 2.0], requires_grad=True)
>>> with torch.no_grad():
...     b = n.pow(2).sum()
...
>>> b
tensor(5.)
>>> b.requires_grad
False
>>> c = a.pow(2).sum()
>>> c.requires_grad
True
上面的例子中,

当a的requires_grad=True时,不使用torch.no_grad(),c.requires_grad为True;

使用torch.no_grad()时,b.requires_grad为False,当不需要进行反向传播时(推断)或不需要计算梯度(网络输入)时,requires_grad=True会占用更多的计算资源及存储资源。

Note: 需要注意的是,torch.no_grad()只是禁止梯度计算,将计算过程中的tensor的requires_grad设置为False,但不会将模型参数如weight这样的tensor的requires_grad设置为False,因为如果训练和测试同时进行,跳出predict的torch.no_grad()后的训练过程还是需要weight中requires_grad=True的。

释放保留的计算图

一个最简单撑爆你的内存的方法是为了记录日志存储你的loss。

losses = []
...
losses.append(loss)

print(f'current loss: torch.mean(losses)')
上面的问题是,loss仍然包含有整个图的副本。在这种情况下,调用.item()来释放它。

# good
losses.append(loss.item())

torch.cuda.empty_cache()

Pytorch 训练时无用的临时变量可能会越来越多,导致 out of memory ,可以使用下面语句来清理这些不需要的变量:torch.cuda.empty_cache() 。
官网 上的解释为:
Releases all unoccupied cached memory currently held by the caching allocator so that those can be used in other GPU application and visible innvidia-smi. 意思就是PyTorch的缓存分配器会事先分配一些固定的显存,即使实际上tensors并没有使用完这些显存,这些显存也不能被其他应用使用。这个分配过程由第一次CUDA内存访问触发的。而 torch.cuda.empty_cache() 的作用就是释放缓存分配器当前持有的且未占用的缓存显存,以便这些显存可以被其他GPU应用程序中使用,并且通过 nvidia-smi命令可见。注意使用此命令不会释放tensors占用的显存。对于不用的数据变量,Pytorch 可以自动进行回收从而释放相应的显存。
更详细的优化可以查查 优化显存使用 和 显存利用问题

 

BUGs

UndefinedMetricWarning: F-score is ill-defined and being set to 0.0 in labels with no predicted samples.'precision', 'predicted', average, warn_for)`

metrics.f1_score(y_test, y_pred, average='weighted')

原因是预测的label都预测成了一个,可能是预测批数据量少了,也可能是模型有问题,从而全部预测成了同一个label。

some labels in y_true don't appear in y_pred. 

[UndefinedMetricWarning: F-score is ill-defined and being set to 0.0 in labels with no predicted samples]

from: -柚子皮-

ref: https://zhuanlan.zhihu.com/p/76459295

[9个技巧让你的PyTorch模型训练变得飞快!]

[使用pytorch时,训练集数据太多达到上千万张,Dataloader加载很慢怎么办?]

以上是关于PyTorch:模型训练和预测的主要内容,如果未能解决你的问题,请参考以下文章

PyTorch:模型训练-模型参数parameters

PyTorch:模型训练-模型参数parameters

PyTorch:模型训练-分布式训练

PyTorch:模型训练-分布式训练

pytorch闪电模型的输出预测

PyTorch:模型save和load