我们在训练神经网络的时候,超参数batch size的大小会对最终的模型效果产生很大的影响。一定条件下,batch size设置的越大,模型就会越稳定。batch size的值通常设置在 8-32 之间,但是当我们做一些计算量需求大的任务(例如语义分割、GAN等)或者输入图片尺寸太大的时候,我们的batch size往往只能设置为2或者4,否则就会出现 “CUDA OUT OF MEMORY” 的不可抗力报错。
贫穷是促进人类进步的阶梯,如何在有限的计算资源的条件下,训练时采用更大的batch size呢?这就是梯度累加(Gradient Accumulation)技术了。
我们以Pytorch为例,一个神经网络的训练过程通常如下:
for i, (inputs, labels) in enumerate(trainloader): optimizer.zero_grad() # 梯度清零 outputs = net(inputs) # 正向传播 loss = criterion(outputs, labels) # 计算损失 loss.backward() # 反向传播,计算梯度 optimizer.step() # 更新参数 if (i+1) % evaluation_steps == 0: evaluate_model()
从代码中可以很清楚地看到神经网络是如何做到训练的:
1.将前一个batch计算之后的网络梯度清零
2.正向传播,将数据传入网络,得到预测结果
3.根据预测结果与label,计算损失值
4.利用损失进行反向传播,计算参数梯度
5.利用计算的参数梯度更新网络参数
下面来看梯度累加是如何做的:
for i, (inputs, labels) in enumerate(trainloader): outputs = net(inputs) # 正向传播 loss = criterion(outputs, labels) # 计算损失函数 loss = loss / accumulation_steps # 损失标准化 loss.backward() # 反向传播,计算梯度 if (i+1) % accumulation_steps == 0: optimizer.step() # 更新参数 optimizer.zero_grad() # 梯度清零 if (i+1) % evaluation_steps == 0: evaluate_model()
1.正向传播,将数据传入网络,得到预测结果
2.根据预测结果与label,计算损失值
3.利用损失进行反向传播,计算参数梯度
4.重复1-3,不清空梯度,而是将梯度累加
5.梯度累加达到固定次数之后,更新参数,然后将梯度清零
总结来讲,梯度累加就是每计算一个batch的梯度,不进行清零,而是做梯度的累加,当累加到一定的次数之后,再更新网络参数,然后将梯度清零。
通过这种参数延迟更新的手段,可以实现与采用大batch size相近的效果。在平时的实验过程中,我一般会采用梯度累加技术,大多数情况下,采用梯度累加训练的模型效果,要比采用小batch size训练的模型效果要好很多。
参考资料
1.Loss Normalization
2.Model.zero_grad() or optimizer.zero_grad()?
3.A trick to use bigger batches for training: gradient accumulation
4.PyTorch中在反向传播前为什么要手动将梯度清零?
5.Training Neural Nets on Larger Batches: Practical Tips for 1-GPU, Multi-GPU & Distributed setups
原文链接:https://cowarder.site/2019/10/29/Gradient-Accumulation/
使用PyTorch实现梯度累加变相扩大batch
PyTorch中在反向传播前为什么要手动将梯度清零? - Pascal的回答 - 知乎
https://www.zhihu.com/question/303070254/answer/573037166
这种模式可以让梯度玩出更多花样,比如说梯度累加(gradient accumulation)
传统的训练函数,一个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()
-
简单的说就是进来一个batch的数据,计算一次梯度,更新一次网络获取loss:输入图像和标签,通过infer计算得到预测值,计算损失函数; -
optimizer.zero_grad()
清空过往梯度; -
loss.backward()
反向传播,计算当前梯度; -
optimizer.step()
根据梯度更新网络参数
使用梯度累加是这么写的:
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个batch的数据,计算1次梯度,梯度不清空,不断累加,累加一定次数后,根据累加的梯度更新网络参数,然后清空梯度,进行下一次循环。
获取loss:输入图像和标签,通过infer计算得到预测值,计算损失函数; -
loss.backward()
反向传播,计算当前梯度; - 多次循环步骤1-2,不清空梯度,使梯度累加在已有梯度上;
- 梯度累加了一定次数后,先
optimizer.step()
根据累计的梯度更新网络参数,然后optimizer.zero_grad()
清空过往梯度,为下一波梯度累加做准备;
一定条件下,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,可以尝试调节下。
借助梯度累加,避免同时计算多个损失时存储多个计算图
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()
上述代码执行到 ******
时,内存中是包含了两张计算图的,而随着求和得到loss,这两张图进行了合并,而且大小的变化可以忽略。从PyTorch的设计原理上来说,在每次进行前向计算得到pred时,会产生一个**用于梯度回传的计算图,这张图储存了进行back propagation需要的中间结果,当调用了 ****.backward()**
后,会从内存中将这张图进行释放。
- 执行到
++++++
时,得到对应的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()
3. Other可以从代码中看出,利用梯度累加,可以在最多保存一张计算图的情况下进行multi-task任务的训练。
另外一个理由就是在内存大小不够的情况下叠加多个batch的grad作为一个大batch进行迭代,因为二者得到的梯度是等价的。
综上可知,这种梯度累加的思路是对内存的极大友好,是由FAIR的设计理念出发的。
相关链接
- PyTorch中在反向传播前为什么要手动将梯度清零? - 知乎:https://www.zhihu.com/question/303070254
- https://discuss.pytorch.org/t/why-do-we-need-to-set-the-gradients-manually-to-zero-in-pytorch/4903/9
本文作者:lart
本文链接:https://www.cnblogs.com/lart/p/11628696.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 *许可协议进行许可。
知识无价。对于有用的知识,请为后来者指明出处。