【PyTorch】PyTorch中的梯度累加

2019年11月10日 阅读数:641
这篇文章主要向大家介绍【PyTorch】PyTorch中的梯度累加,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

PyTorch中的梯度累加

使用PyTorch实现梯度累加变相扩大batch

PyTorch中在反向传播前为何要手动将梯度清零? - Pascal的回答 - 知乎
https://www.zhihu.com/question/303070254/answer/573037166python

这种模式可让梯度玩出更多花样,好比说梯度累加(gradient accumulation)git

传统的训练函数,一个batch是这么训练的:github

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()清空过往梯度;
  3. loss.backward()反向传播,计算当前梯度
  4. optimizer.step()根据梯度更新网络参数

简单的说就是进来一个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.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() 反向传播,计算当前梯度
  3. 屡次循环步骤1-2,不清空梯度,使梯度累加在已有梯度上;
  4. 梯度累加了必定次数后,先 optimizer.step() 根据累计的梯度更新网络参数,而后 optimizer.zero_grad() 清空过往梯度,为下一波梯度累加作准备;

总结来讲:梯度累加就是,每次获取1个batch的数据,计算1次梯度,梯度不清空,不断累加,累加必定次数后,根据累加的梯度更新网络参数,而后清空梯度,进行下一次循环。学习

必定条件下,batchsize越大训练效果越好,梯度累加则实现了batchsize的变相扩大,若是 accumulation_steps 为8,则batchsize '变相' 扩大了8倍,是咱们这种乞丐实验室解决显存受限的一个不错的trick,使用时须要注意,学习率也要适当放大。优化

更新1:关于BN是否有影响,以前有人是这么说的:设计

As far as I know, batch norm statistics get updated on each forward pass, so no problem if you don't do .backward() every time.code

BN的估算是在forward阶段就已经完成的,并不冲突,只是 accumulation_steps=8 和真实的batchsize放大八倍相比,效果天然是差一些,毕竟八倍Batchsize的BN估算出来的均值和方差确定更精准一些。orm

更新2:根据李韶华的分享,能够适当调低BN本身的momentum参数:

bn本身有个momentum参数:x_new_running = (1 - momentum) * x_running + momentum * x_new_observed. momentum越接近0,老的running stats记得越久,因此能够获得更长序列的统计信息

我简单看了下PyTorch 1.0的源码:https://github.com/pytorch/pytorch/blob/162ad945902e8fc9420cbd0ed432252bd7de673a/torch/nn/modules/batchnorm.py#L24,BN类里面momentum这个属性默认为0.1,能够尝试调节下。

借助梯度累加,避免同时计算多个损失时存储多个计算图

PyTorch中在反向传播前为何要手动将梯度清零? - Forever123的回答 - 知乎
https://www.zhihu.com/question/303070254/answer/608153308

缘由在于在PyTorch中,计算获得的梯度值会进行累加。

而这样的好处能够从内存消耗的角度来看。

1. Edition1

在PyTorch中,multi-task任务一个标准的train from scratch流程为:

for idx, data in enumerate(train_loader):
    xs, ys = data
    pred1 = model1(xs)
    pred2 = model2(xs)

    loss1 = loss_fn1(pred1, ys)
    loss2 = loss_fn2(pred2, ys)

    ******
    loss = loss1 + loss2
    optmizer.zero_grad()
    loss.backward()
    ++++++
    optmizer.step()

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

  • 上述代码执行到 ****** 时,内存中是包含了两张计算图的,而随着求和获得loss,这两张图进行了合并,并且大小的变化能够忽略。
  • 执行到 ++++++ 时,获得对应的grad值而且释放内存。这样,训练时必须存储两张计算图,而若是loss的来源组成更加复杂,内存消耗会更大。

2. Edition2

为了减少每次的内存消耗,借助梯度累加,又有 ,有以下变种。

for idx, data in enumerate(train_loader):
    xs, ys = data

    optmizer.zero_grad()

    # 计算d(l1)/d(x)
    pred1 = model1(xs) #生成graph1
    loss = loss_fn1(pred1, ys)
    loss.backward()  #释放graph1

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

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

能够从代码中看出,利用梯度累加,能够在最多保存一张计算图的状况下进行multi-task任务的训练。

3. Other

另一个理由就是在内存大小不够的状况下叠加多个batch的grad做为一个大batch进行迭代,由于两者获得的梯度是等价的。

综上可知,这种梯度累加的思路是对内存的极大友好,是由FAIR的设计理念出发的。

相关连接