理解反向钩子函数

8
我写了下面的代码片段来尝试理解这些钩子发生了什么。
class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.fc1 = nn.Linear(10,5)
        self.fc2 = nn.Linear(5,1)
        self.fc1.register_forward_hook(self._forward_hook)
        self.fc1.register_backward_hook(self._backward_hook)
    
    def forward(self, inp):
        return self.fc2(self.fc1(inp))

    def _forward_hook(self, module, input, output):
        print(type(input))
        print(len(input))
        print(type(output))
        print(input[0].shape)
        print(output.shape)
        print()

    def _backward_hook(self, module, grad_input, grad_output):
        print(type(grad_input))
        print(len(grad_input))
        print(type(grad_output))
        print(len(grad_output))
        print(grad_input[0].shape)
        print(grad_input[1].shape)
        print(grad_output[0].shape)
        print()

model = Model()
out = model(torch.tensor(np.arange(10).reshape(1,1,10), dtype=torch.float32))
out.backward()

生成输出

<class 'tuple'>
1
<class 'torch.Tensor'>
torch.Size([1, 1, 10])
torch.Size([1, 1, 5])

<class 'tuple'>
2
<class 'tuple'>
1
torch.Size([1, 1, 5])
torch.Size([5])
torch.Size([1, 1, 5])

您可以参考CNN的示例此处。事实上,这是理解我后面提出问题所必需的。
我有几个问题:
  1. 通常我认为 grad_input(反向传播钩子)应该与 output(前向传播钩子)形状相同,因为在向后传播时,方向是相反的。但CNN示例似乎表明不是这样。我还有点困惑。到底是哪一种情况呢?

  2. 为什么我的 Linear 层上的 grad_input[0]grad_output[0] 形状相同?无论问题1的答案如何,它们中至少有一个应该是 torch.Size([1, 1, 10]),对吗?

  3. 元组 grad_input 的第二个元素是什么?在CNN的情况下,我复制粘贴了示例并使用 print(grad_input[1].size()) 输出结果为 torch.Size([20, 10, 5, 5])。所以我认为这是权重的梯度。我还运行了 print(grad_input[2].size()),得到的输出是 torch.Size([20])。所以看起来我正在查看偏置的梯度。但是在我的 Linear 示例中,grad_input 的长度为2,所以我只能访问到 grad_input[1],它似乎给出了偏置的梯度。那么权重的梯度在哪里呢?

总之,在Conv2d和`Linear'模块中,向后钩子的行为存在两个明显的矛盾。这让我对这个钩子的预期感到完全困惑。
谢谢你的帮助!
1个回答

10

我通常认为 grad_input(反向勾)应该与输出具有相同的形状。

grad_input 包含对层的 input 的梯度(在进行机器学习时通常是损失张量,对于您来说,它只是 Model 的输出),因此其形状与 input 相同。同样,grad_output 与层的 output 具有相同的形状。这也适用于您引用的 CNN 示例 cited

为什么我的这个 Linear 层的 grad_input[0] 和 grad_output[0] 形状相同?无论问题 1 的答案是什么,它们中至少有一个应该是 torch.Size([1, 1, 10]) 对吗?

理想情况下,grad_input 应该包含相对于层输入、权重和偏差的梯度。如果您对CNN示例使用以下反向钩子,则会看到这种行为:
def _backward_hook(module, grad_input, grad_output): 
     for i, inp in enumerate(grad_input): 
         print("Input #", i, inp.shape) 

然而,Linear层不会出现这种情况,这是由于错误。顶级评论指出
“模块挂钩实际上是在模块创建的最后一个函数上注册的。”
所以后端可能真正发生的事情是它正在计算Y=((W^TX)+b)。您可以看到添加偏差是最后一次操作。因此,对于该操作,有一个形状为(1,1,5)的输入和形状为(5)的偏差项。这两个(实际上是它们的梯度)形成了元组grad_input。加法的结果(实际上是它的梯度)存储在形状为(1,1,5)的grad_output中。
“元组grad_input的第二个元素是什么?”

正如上面所回答的那样,它只是对“层参数”梯度的梯度进行计算;通常是最后一次操作的权重/偏置(如果适用)。


啊,这是一个bug...谢谢! - Alexander Soare

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