前提条件:
已经安装好了python及torch环境。
示例代码:
inp = torch.eye(4, 5, requires_grad=True)
out = (inp+1).pow(2).t()
out.backward(torch.ones_like(out), retain_graph=True)
print(f"First call\n{inp.grad}")
out.backward(torch.ones_like(out), retain_graph=True)
print(f"\nSecond call\n{inp.grad}")
inp.grad.zero_()
out.backward(torch.ones_like(out), retain_graph=True)
print(f"\nCall after zeroing gradients\n{inp.grad}")
代码详细解读:
这段代码展示了PyTorch中自动求导(autograd)的基本原理,特别是梯度计算、累积和清零的过程。我们逐步解析:
-
创建输入张量
inp = torch.eye(4, 5, requires_grad=True)- 创建一个4行5列的单位矩阵(对角线上为1,其余为0)
requires_grad=True表示需要跟踪该张量的计算历史并计算梯度
-
定义计算过程
out = (inp+1).pow(2).t()对输入张量进行一系列操作:
inp+1:每个元素加1(单位矩阵变成对角线上为2,其余为1).pow(2):每个元素平方(对角线上为4,其余为1).t():矩阵转置(变成5行4列)
-
第一次反向传播
out.backward(torch.ones_like(out), retain_graph=True)torch.ones_like(out)是梯度的初始值(外部梯度),形状与out相同的全1矩阵retain_graph=True表示保留计算图,以便后续再次反向传播- 计算图会自动计算
out对inp的梯度,并存储在inp.grad中
根据链式法则,求导结果为:
d(out)/d(inp) = 2*(inp+1)
所以梯度矩阵中,原单位矩阵对角线位置为2*(1+1)=4,其余位置为2*(0+1)=2 -
第二次反向传播
out.backward(torch.ones_like(out), retain_graph=True)- 再次计算梯度时,PyTorch会累积梯度(将新计算的梯度加到已有梯度上)
- 此时
inp.grad中的值是第一次梯度的2倍(对角线8,其余4)
-
梯度清零后再次传播
inp.grad.zero_() # 清零梯度 out.backward(torch.ones_like(out), retain_graph=True)zero_()方法将梯度重置为0- 再次传播后,梯度回到第一次计算的结果(对角线4,其余2)
输出结果说明:
- 第一次打印:梯度矩阵中对角线为4,其余为2
- 第二次打印:梯度累积后,对角线为8,其余为4
- 清零后打印:梯度重置,回到对角线4,其余为2
这个例子清晰展示了PyTorch中梯度的计算机制:默认会累积梯度,因此在训练神经网络时,通常需要在每个batch训练前使用zero_()方法清零梯度。
提问环节:
Q1:第二次反向传播结果为什么是8和4?
核心前提
PyTorch中,backward()会默认将新计算的梯度累加到已有梯度上(即 grad = 旧梯度 + 新梯度),而非覆盖。这是第二次传播后梯度变为8的根本原因。
详细计算步骤
1. 定义计算图与求导公式
已知计算过程:
out = (inp + 1).pow(2).t() # t()是转置,不影响元素值,仅改变形状
简化后,每个元素的计算关系为:
out_ij = (inp_ji + 1)²(转置导致索引互换,不影响导数大小)
对inp中任意元素inp_xy求导(链式法则):
∂out / ∂inp_xy = 2*(inp_xy + 1)
2. 初始输入inp的值
inp = torch.eye(4, 5) # 4行5列单位矩阵
inp的非零元素仅在对角线(i=j时)为1,其余为0:
inp = [
[1, 0, 0, 0, 0],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0],
[0, 0, 0, 1, 0]
]
3. 第一次反向传播的梯度计算
-
调用
out.backward(torch.ones_like(out)),其中torch.ones_like(out)是全1的梯度初始值(相当于对out的每个元素求导权重为1)。 -
根据求导公式
∂out / ∂inp_xy = 2*(inp_xy + 1):- 对角线元素(
inp_xy=1):2*(1+1) = 4 - 非对角线元素(
inp_xy=0):2*(0+1) = 2
因此,第一次传播后
inp.grad为:[ [4, 2, 2, 2, 2], [2, 4, 2, 2, 2], [2, 2, 4, 2, 2], [2, 2, 2, 4, 2] ] - 对角线元素(
4. 第二次反向传播的梯度累积
-
再次调用
out.backward(torch.ones_like(out))时:- 新计算的梯度与第一次完全相同(因为
inp的值未改变):对角线4,非对角线2。 - PyTorch自动执行梯度累积:
新梯度 = 旧梯度 + 新计算的梯度。
因此,第二次传播后
inp.grad为:- 对角线元素:
4(旧) + 4(新) = 8 - 非对角线元素:
2(旧) + 2(新) = 4
结果为:
[ [8, 4, 4, 4, 4], [4, 8, 4, 4, 4], [4, 4, 8, 4, 4], [4, 4, 4, 8, 4] ] - 新计算的梯度与第一次完全相同(因为
结论
第二次反向传播中出现的8,是第一次梯度(4)与第二次梯度(4)累积相加的结果,体现了PyTorch梯度自动累积的特性。这也是训练神经网络时,每个batch前需要用zero_()清零梯度的原因(避免历史梯度干扰当前计算)。
Q2:第三次为啥又和第一次一样了?
第三次反向传播是在执行了梯度清零操作(inp.grad.zero_())之后进行的,这个过程很好地展示了梯度清零的作用。我们详细拆解计算步骤:
1. 梯度清零操作
在第三次反向传播前,执行了:
inp.grad.zero_() # 将inp的梯度张量所有元素重置为0
此时,inp.grad 中的值不再是第二次传播后的累积结果(对角线8、非对角线4),而是全部变为0:
inp.grad = [
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]
]
2. 第三次反向传播的梯度计算
再次调用:
out.backward(torch.ones_like(out), retain_graph=True)
此时的计算逻辑与第一次反向传播完全相同,原因是:
inp本身的值未发生改变(始终是初始的4x5单位矩阵)- 计算图未变(
out = (inp+1).pow(2).t()的关系不变) - 反向传播的初始梯度仍为
torch.ones_like(out)(全1矩阵)
根据求导公式 ∂out / ∂inp_xy = 2*(inp_xy + 1):
- 对角线元素(
inp_xy=1):2*(1+1) = 4 - 非对角线元素(
inp_xy=0):2*(0+1) = 2
3. 第三次传播后的梯度结果
由于之前执行了 zero_(),梯度不会累积,而是直接存储本次计算的结果:
inp.grad = [
[4, 2, 2, 2, 2],
[2, 4, 2, 2, 2],
[2, 2, 4, 2, 2],
[2, 2, 2, 4, 2]
]
这个结果与第一次反向传播的输出完全一致,说明梯度清零后,新的反向传播会重新计算梯度,而不受历史梯度的影响。
总结
第三次反向传播的核心是梯度清零的作用验证:
- 清零后,梯度不再累积历史值,而是从0开始计算
- 结果与第一次传播完全相同,证明每次反向传播的梯度计算逻辑一致
- 这正是神经网络训练中,每个batch前需要清零梯度的原因(确保每个batch的梯度独立计算,不被上一个batch干扰)
954

被折叠的 条评论
为什么被折叠?



