• PyTorch会在每次.backward()调用时会累积梯度的问题


    代码

    # backward() accumulates the gradient for this tensor into .grad attribute.
    # !!! We need to be careful during optimization !!!
    # Use .zero_() to empty the gradients before a new optimization step!
    weights = torch.ones(4, requires_grad=True)
    
    for epoch in range(3):
        # just a dummy example
        model_output = (weights*3).sum()
        model_output.backward()
        
        print(weights.grad)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这段代码展示了在使用PyTorch进行梯度计算和优化时的一个典型模式,包括如何累积梯度、如何在每一步优化前清空梯度,以及为何这样做是重要的。下面是对代码各部分的解释:

    初始化权重

    weights = torch.ones(4, requires_grad=True)
    
    • 1

    这行代码创建了一个长度为4的张量,其初始值全为1,并设置requires_grad=True。这表示weights是需要计算梯度的,即在反向传播时,PyTorch会自动计算这些权重的梯度并存储在它们的.grad属性中。

    循环和模型输出

    for epoch in range(3):
        # just a dummy example
        model_output = (weights*3).sum()
        model_output.backward()
        
        print(weights.grad)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这段代码表示了一个简化的训练循环,循环次数(代表“epoch”)为3。在每次循环中:

    • 计算模型输出:通过将weights乘以3再求和得到model_output。这里的操作(乘以3和求和)仅仅是为了示例,并不代表实际模型的复杂度。
    • 执行model_output.backward():这个调用进行自动梯度计算,计算model_output关于所有requires_grad=True的张量(在此例中是weights)的梯度,并将计算得到的梯度累积到weights.grad属性中。
    • 打印weights.grad:展示了当前权重的梯度。

    梯度累积和清空

    代码的关键点之一是PyTorch在每次.backward()调用时累积梯度。这意味着如果不手动清空梯度,那么每次调用.backward()时计算得到的梯度就会加到已有的梯度上,这通常不是我们想要的行为,因为它会导致梯度值不断增加,从而影响到优化过程。

    尽管这段示例代码中没有直接展示清空梯度的操作,但在注释中提到了使用.zero_()方法来清空梯度是非常重要的一步:

    # Use .zero_() to empty the gradients before a new optimization step!
    
    • 1

    在实际应用中,正确的做法是在每次优化步骤之前调用weights.grad.zero_()来清空梯度,以避免梯度累积导致的问题。这样可以确保每一步的优化都是基于最新一次前向传播计算得到的梯度。

    总结

    这段代码展示了在PyTorch中如何计算梯度、梯度累积的特性以及清空梯度的重要性。在实际训练模型时,适时清空梯度是保证模型正确学习的关键步骤之一。

    输出:

    tensor ( [3., 3., 3., 3.])
    tensor ( [6., 6., 6., 6.])
    tensor([9., 9., 9., 9.])
    
    • 1
    • 2
    • 3

    这段代码的输出展示了在三个训练周期(epoch)内,权重梯度的累积情况。由于在每次循环结束时没有清空梯度,所以得到的是梯度随着每个训练周期而逐步增加的结果。

    代码回顾

    代码中的关键操作是:

    • 计算模型输出model_output = (weights*3).sum()
    • 调用model_output.backward()来计算梯度

    输出解释

    1. 第一次迭代:权重梯度被初始化为0。当执行model_output.backward()后,计算得到的梯度(对每个权重的梯度是3)被累加到weights.grad中,因此打印出[3., 3., 3., 3.]
    2. 第二次迭代:由于之前的梯度没有被清空,新计算的梯度值(同样是[3., 3., 3., 3.])被添加到现有的梯度上,结果是每个权重的梯度增加到了6,打印出[6., 6., 6., 6.]
    3. 第三次迭代:这个过程再次发生,新的梯度值再次被添加到现有的梯度上,导致每个权重的梯度增加到了9,打印出[9., 9., 9., 9.]

    梯度累积原理

    在PyTorch中,.backward()方法计算的梯度会累加到张量的.grad属性中,而不是替换它。这意味着如果不手动清空梯度(使用.zero_()方法),每次.backward()调用的梯度就会叠加起来。这个特性在某些情况下是有用的,比如当你想要在多个小批量(minibatches)上累积梯度时。然而,在大多数情况下,在每次迭代前清空梯度是必要的,以避免因错误累积梯度而导致的优化问题。

    为何梯度是3

    在这个特定的例子中,梯度值为3的原因是模型输出model_output是权重weights乘以3后的值的和。因此,model_output关于每个权重的梯度就是3,这是由于求和操作的导数为1,且每个权重被乘以3。

    为了更好地理解为什么梯度是3,我们可以详细地通过计算过程来阐述。

    考虑模型输出 model_output 的计算公式,它是通过权重 weights 乘以一个常数(在这个例子中是3),然后对结果求和来得到的。如果我们将权重表示为 ( w 1 , w 2 , w 3 , w 4 ) (w_1, w_2, w_3, w_4) (w1,w2,w3,w4)(因为 weights 初始化为一个长度为4的张量),模型输出 model_output 可以表示为:

    m o d e l _ o u t p u t = 3 w 1 + 3 w 2 + 3 w 3 + 3 w 4 model\_output = 3w_1 + 3w_2 + 3w_3 + 3w_4 model_output=3w1+3w2+3w3+3w4

    我们要计算的是模型输出 model_output 关于每个权重的梯度,即 ( ∂ m o d e l _ o u t p u t ∂ w i ) (\frac{\partial model\_output}{\partial w_i}) (wimodel_output),其中 i i i 是权重的索引(1, 2, 3, 4)。

    根据导数的定义,对于每个 w i w_i wi,其梯度计算如下:

    ∂ m o d e l _ o u t p u t ∂ w 1 = 3 \frac{\partial model\_output}{\partial w_1} = 3 w1model_output=3
    ∂ m o d e l _ o u t p u t ∂ w 2 = 3 \frac{\partial model\_output}{\partial w_2} = 3 w2model_output=3
    ∂ m o d e l _ o u t p u t ∂ w 3 = 3 \frac{\partial model\_output}{\partial w_3} = 3 w3model_output=3
    ∂ m o d e l _ o u t p u t ∂ w 4 = 3 \frac{\partial model\_output}{\partial w_4} = 3 w4model_output=3

    这是因为 model_output 相对于每个 w i w_i wi 的导数是3,即每个权重的系数。当你在代码中执行 model_output.backward() 时,PyTorch 自动计算这些导数(梯度),并将结果存储在 weights.grad 中。

    因此,无论是第一次迭代还是随后的迭代,每次调用 model_output.backward() 都会在之前的基础上累加梯度3到 weights.grad 中,这就是为什么你看到梯度从 [3., 3., 3., 3.] 开始,每次迭代后都递增3的原因。这也解释了为何在每次迭代之前重置梯度是必要的,以防止错误累积导致的问题。

  • 相关阅读:
    Qt+ECharts开发笔记(三):ECharts的柱状图介绍、基础使用和Qt封装Demo
    java8使用stream流将list转map
    【LeetCode】No.75. Sort Colors -- Java Version
    深入解析HTTPS与HTTP
    两分钟了解Http常见状态码
    upload-labs通关(Pass01-Pass05)
    React 18 对 state 进行保留和重置
    记一次使用nacos2踩到的坑
    fastapi_No.24_日志记录系统
    33、CSS进阶——布局
  • 原文地址:https://blog.csdn.net/qq_37397652/article/details/136594273