为什么我们需要在PyTorch中调用zero_grad()函数?

338

在训练期间为什么需要调用zero_grad()

|  zero_grad(self)
|      Sets gradients of all model parameters to zero.
7个回答

474
PyTorch中,在训练阶段的每个小批量中,我们通常希望在开始进行反向传播(即更新权重偏置)之前明确将梯度设置为零,因为PyTorch在后续的反向传递中累积梯度。这种累积行为在训练RNN或者我们想要计算损失在多个小批量上求和的梯度时非常方便。因此,默认操作已设置为在每次loss.backward()调用时累加(即求和)梯度
因此,当您开始训练循环时,理想情况下应该将梯度清零,以便正确地进行参数更新。否则,梯度将是旧梯度和新计算梯度的组合。因此,它将指向与预期方向不同的方向,朝向最小值(或最大值,在最大化目标的情况下)。

这里是一个简单的例子:

import torch
from torch.autograd import Variable
import torch.optim as optim

def linear_model(x, W, b):
    return torch.matmul(x, W) + b

data, targets = ...

W = Variable(torch.randn(4, 3), requires_grad=True)
b = Variable(torch.randn(3), requires_grad=True)

optimizer = optim.Adam([W, b])

for sample, target in zip(data, targets):
    # clear out the gradients of all Variables 
    # in this optimizer (i.e. W, b)
    optimizer.zero_grad()
    output = linear_model(sample, W, b)
    loss = (output - target) ** 2
    loss.backward()
    optimizer.step()

或者,如果你正在进行一个“纯粹的梯度下降”,那么:

W = Variable(torch.randn(4, 3), requires_grad=True)
b = Variable(torch.randn(3), requires_grad=True)

for sample, target in zip(data, targets):
    # clear out the gradients of Variables 
    # (i.e. W, b)
    W.grad.data.zero_()
    b.grad.data.zero_()

    output = linear_model(sample, W, b)
    loss = (output - target) ** 2
    loss.backward()

    W -= learning_rate * W.grad.data
    b -= learning_rate * b.grad.data

注意:


4
非常感谢,这真的很有帮助!你知道tensorflow是否具有该行为吗? - layser
26
如果我们累积梯度,这并不意味着它们的大小会增加:例如,如果梯度的符号不断变化,那么它们的大小不会增加。因此,它不能保证您会遇到梯度爆炸问题。此外,即使正确归零,梯度爆炸问题仍然存在。 - Tom Roth
1
关于这个问题的后续:你的意思是说,当训练RNN模型(如LSTM)时,我们不应该调用optimizer.zero_grad()吗? - Loqz
1
为什么 optimizer.zero_grad()output = linear_model(sample, W, b) 之前? - mrgloom
1
有人能回答@Loqz的问题吗?我也很想知道。在训练RNN时,需要调用zero_grad()吗? - Alaa M.
显示剩余8条评论

25
虽然这个想法可以从所选答案中推导出来,但我觉得我想要明确地写出来。
能够决定何时调用optimizer.zero_grad()optimizer.step()在训练循环中提供了更多关于梯度如何被优化器累积和应用的自由。当模型或输入数据很大,并且一个训练批次无法适应GPU时,这一点至关重要。
this example中,有两个参数,分别是train_batch_sizegradient_accumulation_steps
  • train_batch_size是前向传播后的批处理大小,遵循loss.backward()。这受到GPU内存的限制。

  • gradient_accumulation_steps是实际的训练批次大小,多次前向传播的损失会累积起来。这不受GPU内存的限制。

从这个例子中,你可以看到如何使用optimizer.zero_grad()后面跟着optimizer.step(),但不是loss.backward()。在每次迭代(第216行)中都会调用loss.backward(),但只有当累积的训练批次数等于gradient_accumulation_steps时(第219行中的if块内的第227行),才会调用optimizer.zero_grad()optimizer.step()
此外,有人问关于TensorFlow中的等效方法。我猜tf.GradientTape具有相同的目的。

谢谢您提供这个例子,它对我很有帮助。 - yotabyte
这与在GPU内存有限的情况下训练大型模型有关。您的想法在这篇不错的文章中得到了扩展:https://towardsdatascience.com/i-am-so-done-with-cuda-out-of-memory-c62f42947dca - Under-qualified NASA Intern

6
为什么要使用梯度?
梯度向优化器“建议”应该朝哪个方向前进。每次使用.backward()处理一批输入时,你会累积关于前进方向的“建议”。需要注意的是,这种建议比“决策”要弱得多。当你调用optimizer.step()时,优化器会利用这些建议来做出实际的“决策”,确定应该前进的位置。这些决策可能受到学习率、过去的步骤(如动量)和过去的权重(如SWA)的影响。优化器读取这些建议,然后朝着希望能够最小化未来损失的方向前进。
loss.backward()        # Compute gradients.
optimizer.step()       # Tell the optimizer the gradients, then step.
optimizer.zero_grad()  # Zero the gradients to start fresh next time.

为什么要将梯度清零?
一旦完成了一步,你实际上不需要继续追踪之前的建议(即梯度),告诉你应该往哪个方向前进。通过将梯度清零,你抛弃了这些信息。有些优化器已经自动内部地保留了这些信息。
在下一批输入中,你从一个干净的起点开始,纯粹地提出下一步应该往哪个方向前进的建议。这个建议是纯粹的,不受过去的影响。然后,你将这个“纯粹”的信息提供给优化器,优化器决定确切的下一步应该往哪个方向前进。
当然,你可以选择保留之前的梯度,但是那些信息在你处于损失曲面上的一个全新位置时已经有些过时了。谁能说下一步最好的方向仍然与之前相同呢?可能完全不同!这就是为什么大多数流行的优化算法会丢弃大部分过时的信息(通过将梯度清零)。

另一种选择:完全删除梯度(而不是置零)
不仅可以将梯度置零,还可以完全删除它们。PyTorch性能调优指南建议如下:
# INSTEAD OF:
model.zero_grad()
# or
optimizer.zero_grad()

# CONSIDER:
for param in model.parameters():
    param.grad = None

...但是其中一位开发者在5年前的评论中提到了this

主要区别在于包含梯度的张量不会在每次反向传播时重新分配内存。由于内存分配相当昂贵(尤其在GPU上),这样更加高效。

还有其他微妙的差异,比如一些优化器在梯度为0或None时表现不同。我确信还有其他地方也会有类似的行为。

...另一方面,原地操作通常被认为不是必要的,甚至在某些情况下可能是次优的,所以我想对于两种方法的性能会有所不同。


2
简单来说,我们需要ZERO_GRAD,因为当我们开始一个训练循环时,我们不希望过去的梯度或过去的结果干扰我们当前的结果。这是因为PyTorch的工作方式是在反向传播时收集/累积梯度,如果过去的结果混在一起,可能会给出错误的结果,所以我们每次循环时都将梯度设为零。 下面是一个例子:

    # let us write a training loop
    torch.manual_seed(42)
    
    epochs = 200
    for epoch in range(epochs):
      model_1.train()
    
      y_pred = model_1(X_train)
    
      loss = loss_fn(y_pred,y_train)
    
      optimizer.zero_grad()
    
      loss.backward()

      optimizer.step()

在这个for循环中,如果我们不每次将优化器设置为零,过去的值可能会累加并改变结果。因此,我们使用zero_grad来避免错误的累积结果。

2

zero_grad() 方法用于清空梯度并从上一步重新开始迭代,如果你使用梯度方法减少误差或损失。

如果你不使用 zero_grad(),损失将会增加而不是减少,与所需相反。

例如:

如果你使用 zero_grad(),你将获得以下输出:

model training loss is 1.5
model training loss is 1.4
model training loss is 1.3
model training loss is 1.2

如果您不使用zero_grad(),则将获得以下输出:
model training loss is 1.4
model training loss is 1.9
model training loss is 2
model training loss is 2.8
model training loss is 3.5

9
最起码可以说这很令人困惑。哪些循环会重新开始?损失的增加/减少受到间接影响,当您执行.zero_grad()时它可能会增加,而不执行该操作则可能会减少。您展示的输出结果是从哪里来的? - dedObed
亲爱的 dedObed(如果您从正确的代码中删除了 zero_grad ),我们正在讨论 .zero_grad() 函数,该函数仅在没有最后结果的情况下开始循环。如果损失在增加,则应该检查您的输入(将问题写在新主题中并给我链接)。 - Youssri Abo Elseod
7
我认为我足够了解PyTorch。我只是指出了您回答中我认为存在的缺陷——不够清晰,得出了过于草率的结论,并展示了一些来源不明的输出。 - dedObed

0

在前向传播期间,权重被分配给输入,在第一次迭代后,权重被初始化为模型从样本(输入)中学到的内容。当我们开始反向传播时,我们希望更新权重以获得最小化成本函数的损失。因此,我们清除先前的权重,以获得更好的权重。这是我们在训练中不断进行的操作,而在测试中我们不执行此操作,因为我们已经在训练时间内获得了最适合我们数据的权重。希望这能更清楚!


0

你不必反复调用grad_zero(),其中一种方法是通过衰减梯度来实现:

optimizer = some_pytorch_optimizer
# decay the grads :
for group in optimizer.param_groups:
    for p in group['params']:
        if p.grad is not None:
            ''' original code from git:
            if set_to_none:
                p.grad = None
            else:
                if p.grad.grad_fn is not None:
                    p.grad.detach_()
                else:
                    p.grad.requires_grad_(False)
                p.grad.zero_()
                
            '''
            p.grad = p.grad / 2

这样学习就更加连续了


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接