知行编程网知行编程网  2022-04-05 12:00 知行编程网 隐藏边栏 |   抢沙发  83 
文章评分 0 次,平均分 0.0

点击上方“MLNLP”,选择“星标”

重磅干货,第一时间送达

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

编辑:忆臻

https://www.zhihu.com/question/303070254

本文仅作为学术分享,如果侵权,会删文处理

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


作者:Pascal
https://www.zhihu.com/question/303070254/answer/573037166

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

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

    
<span style="color:#cc7832;">for </span>i<span style="color:#cc7832;">,</span>(images<span style="color:#cc7832;">,</span>target) <span style="color:#cc7832;">in </span><span style="color:#8888c6;">enumerate</span>(train_loader):<br  />    <span style="color:#808080;"># 1. input output<br  /></span><span style="color:#808080;">    </span>images = images.cuda(<span style="color:#aa4926;">non_blocking</span>=<span style="color:#cc7832;">True</span>)<br  />    target = torch.from_numpy(np.array(target)).float().cuda(<span style="color:#aa4926;">non_blocking</span>=<span style="color:#cc7832;">True</span>)<br  />    outputs = model(images)<br  />    loss = criterion(outputs<span style="color:#cc7832;">,</span>target)<br  /><br  />    <span style="color:#808080;"># 2. backward<br  /></span><span style="color:#808080;">    </span>optimizer.zero_grad()   <span style="color:#808080;"># reset gradient<br  /></span><span style="color:#808080;">    </span>loss.backward()<br  />    optimizer.step()
       
  1. 获取loss:输入图像和标签,通过infer计算得到预测值,计算损失函数;

  2. optimizer.zero_grad() 清空过往梯度;

  3. loss.backward() 反向传播,计算当前梯度;

  4. optimizer.step() 根据梯度更新网络参数

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


使用梯度累加是这么写的:

<span style="color:#cc7832;">for </span>i<span style="color:#cc7832;">,</span>(images<span style="color:#cc7832;">,</span>target) <span style="color:#cc7832;">in </span><span style="color:#8888c6;">enumerate</span>(train_loader):<br  />    <span style="color:#808080;"># 1. input output<br  /></span><span style="color:#808080;">    </span>images = images.cuda(<span style="color:#aa4926;">non_blocking</span>=<span style="color:#cc7832;">True</span>)<br  />    target = torch.from_numpy(np.array(target)).float().cuda(<span style="color:#aa4926;">non_blocking</span>=<span style="color:#cc7832;">True</span>)<br  />    outputs = model(images)<br  />    loss = criterion(outputs<span style="color:#cc7832;">,</span>target)<br  /><br  />    <span style="color:#808080;"># 2.1 loss regularization<br  /></span><span style="color:#808080;">    </span>loss = loss/accumulation_steps<br  />    <span style="color:#808080;"># 2.2 back propagation<br  /></span><span style="color:#808080;">    </span>loss.backward()<br  />    <span style="color:#808080;"># 3. update parameters of net<br  /></span><span style="color:#808080;">    </span><span style="color:#cc7832;">if</span>((i+<span style="color:#6897bb;">1</span>)%accumulation_steps)==<span style="color:#6897bb;">0</span>:<br  />        <span style="color:#808080;"># optimizer the net<br  /></span><span style="color:#808080;">        </span>optimizer.step()        <span style="color:#808080;"># update parameters of net<br  /></span><span style="color:#808080;">        </span>optimizer.zero_grad()   <span style="color:#808080;"># reset gradient</span>
  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.

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

更新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,可以尝试调节下。


作者:Forever123
https://www.zhihu.com/question/303070254/answer/608153308

原因在于在PyTorch中,计算得到的梯度值会进行累加

而这样的好处可以从内存消耗的角度来看

1. Edition1

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

<span style="color:#cc7832;">for </span>idx<span style="color:#cc7832;">, </span>data <span style="color:#cc7832;">in </span><span style="color:#8888c6;">enumerate</span>(train_loader):<br  />    xs<span style="color:#cc7832;">, </span>ys = data<br  />    pred1 = model1(xs)<br  />    pred2 = model2(xs)<br  /><br  />    loss1 = loss_fn1(pred1<span style="color:#cc7832;">, </span>ys)<br  />    loss2 = loss_fn2(pred2<span style="color:#cc7832;">, </span>ys)<br  /><br  />    ** ** **<br  />    loss = loss1 + loss2<br  />    optmizer.zero_grad()<br  />    loss.backward()<br  />    ++++++<br  />    optmizer.step()

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

上述代码执行到******时,内存中是包含了两张计算图的,而随着求和得到loss,这两张图进行了合并,而且大小的变化可以忽略

执行到++++++时,得到对应的grad值并且释放内存。这样,训练时必须存储两张计算图,而如果loss的来源组成更加复杂,内存消耗会更大

2. Edition2

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

<span style="color:#cc7832;">for </span>idx<span style="color:#cc7832;">, </span>data <span style="color:#cc7832;">in </span><span style="color:#8888c6;">enumerate</span>(train_loader):<br  />    xs<span style="color:#cc7832;">, </span>ys = data<br  /><br  />    optmizer.zero_grad()<br  />    <span style="color:#808080;"># 计算d(l1)/d(x)<br  /></span><span style="color:#808080;">    </span>pred1 = model1(xs)  <span style="color:#808080;"># 生成graph1<br  /></span><span style="color:#808080;">    </span>loss = loss_fn1(pred1<span style="color:#cc7832;">, </span>ys)<br  />    loss.backward()  <span style="color:#808080;"># 释放graph1<br  /></span><span style="color:#808080;">    # 计算d(l2)/d(x)<br  /></span><span style="color:#808080;">    </span>pred2 = model2(xs)  <span style="color:#808080;"># 生成graph2<br  /></span><span style="color:#808080;">    </span>loss2 = loss_fn2(pred2<span style="color:#cc7832;">, </span>ys)<br  />    loss.backward()  <span style="color:#808080;"># 释放graph2<br  /></span><span style="color:#808080;">    # 使用d(l1)/d(x)+d(l2)/d(x)进行优化<br  /></span><span style="color:#808080;">    </span>optmizer.step()

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

另外一个理由就是在内存大小不够的情况下叠加多个batch的grad作为一个大batch进行迭代,因为二者得到的梯度是等价的

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


作者:blateyang
https://www.zhihu.com/question/303070254/answer/535552845


简单的理由是因为PyTorch默认会对梯度进行累加。


至于为什么PyTorch有这样的特点,在网上找到的解释是说由于PyTorch的动态图和autograd机制使得其非常灵活,这也意味着你可以得到对一个张量的梯度,然后再次用该梯度进行计算,然后又可重新计算对新操作的梯度,对于何时停止前向操作并没有一个确定的点。所以自动设置梯度为0比较棘手,因为你不知道什么时候一个计算会结束以及什么时候又会有一个新的开始。


默认累加的好处是当在多任务中对前面共享部分的tensor进行了多次计算操作后,调用不同任务loss的backward,那些tensor的梯度会自动累加,缺点是当你不想先前的梯度影响到当前梯度的计算时需要手动清零。


—完—

为您推荐

10张 GIF 动图让你弄懂递归等概念
纯国产大佬周志华,如何扛起了智能学界大旗
Nature发文:避开机器学习三大「坑」
如何利用Python开发人工智能入门项目
【微软】AI-神经网络基本原理简明教程

本篇文章来源于: 深度学习这件小事

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

知行编程网
知行编程网 关注:1    粉丝:1
这个人很懒,什么都没写

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享