pytorch hook 钩子
Posted fnangle
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了pytorch hook 钩子相关的知识,希望对你有一定的参考价值。
简介
hook是钩子,主要作用是不修改主代码,能通过挂载钩子实现额外功能。 pytorch中,主体就是forward和backward,而额外的功能就是对模型的变量进行操作,如“提取”特征图,“提取”非叶子张量的梯度,修改张量梯度等等。hook功能即不必改变网络输入输出的结构,就能方便地获取、改变网络中间层变量的值和梯度。这个功能被广泛用于可视化神经网络中间层的 feature、gradient。
tensor的hook;对module的前向、反向hook,一般来说共有三种hook。
下面的计算图中,x y w 为叶子节点,而 z 为中间变量
pytorch的计算图中只有输出对叶子结点变量的梯度被保存下来, 所有中间变量的梯度只被用于反向传播,一旦完成反向传播,中间变量的梯度就将自动释放(虽然 requires_grad 的参数都是 True),从而节约内存。 获取中间节点梯度还可以用 retain_grad(),但这样也会会增加内存占用。
torch.Tensor.register_hook(hook_fn)
hook_fn(grad) -> Tensor or None ,其中grad就是这个tensor的梯度。
hook_fn是我们自定义的函数,假设对上图中间节点z的hook_fn函数来说,输入为变量 z 的梯度,输出为一个 Tensor 或者是 None (None 一般用于直接打印梯度)。反向传播时,梯度传播到变量 z,再继续向前传播之前,将会传入 hook_fn。如果hook_fn的返回值是 None,那么梯度将不改变,继续向前传播,如果 hook_fn的返回值是 Tensor 类型,则该 Tensor 将取代 z 原有的梯度,向前传播。改变中间变量的梯度,之前变量的梯度也会收到影响(变量x,y)。
import torch
def grad_hook(grad): y_grad.append(grad)
y_grad = list() x = torch.tensor([[1., 2.], [3., 4.]], requires_grad=True) y = x+1 y.register_hook(grad_hook)
out = torch.mean(y*y) out.backward() print("type(y): ", type(y)) print("y.grad: ", y.grad) print("y_grad[0]: ", y_grad[0]) >>> (\'type(y): \', <class \'torch.Tensor\'>) >>> (\'y.grad: \', None) >>> (\'y_grad[0]: \', tensor([[1.0000, 1.5000], [2.0000, 2.5000]]))
上述代码中,x是叶子结点,y是中间节点,反向传播完成,out对y的梯度y.grad=None 证明中间节点梯度被释放。
而通过自定义的hook函数:grad_hook 把y的梯度保存到全局变量:y_grad = list()中。 因此可以在out.backward()结束后,仍旧可以在y_grad[0]中读到y的梯度为tensor([0.2500, 0.2500, 0.2500, 0.2500])
下面是一个修改grad的hook:
w = torch.tensor([1.], requires_grad=True) x = torch.tensor([2.], requires_grad=True) a = torch.add(w, x) b = torch.add(w, 1) y = torch.mul(a, b) a_grad = list() def grad_hook(grad): grad *= 2 return grad*3 handle = w.register_hook(grad_hook) y.backward() # 查看梯度 print("w.grad: ", w.grad) handle.remove() # w.grad: tensor([30.])=5*2*3
nn.Module.register_forward_hook(hook_fn)
hook_fn(module, input, output) -> None。注意不能修改input和output
import torch from torch import nn # 首先我们定义一个模型 class Model(nn.Module): def __init__(self): super(Model, self).__init__() self.fc1 = nn.Linear(3, 4) self.relu1 = nn.ReLU() self.fc2 = nn.Linear(4, 1) self.initialize() # 为了方便验证,我们将指定特殊的weight和bias def initialize(self): with torch.no_grad(): self.fc1.weight = torch.nn.Parameter( torch.Tensor([[1., 2., 3.], [-4., -5., -6.], [7., 8., 9.], [-10., -11., -12.]])) self.fc1.bias = torch.nn.Parameter(torch.Tensor([1.0, 2.0, 3.0, 4.0])) self.fc2.weight = torch.nn.Parameter(torch.Tensor([[1.0, 2.0, 3.0, 4.0]])) self.fc2.bias = torch.nn.Parameter(torch.Tensor([1.0])) def forward(self, x): o = self.fc1(x) o = self.relu1(o) o = self.fc2(o) return o # 全局变量,用于存储中间层的 feature total_feat_out = [] total_feat_in = [] # 定义 forward hook function def hook_fn_forward(module, input, output): print(module) # 用于区分模块 print(\'input\', input) # 首先打印出来 print(\'output\', output) total_feat_out.append(output) # 然后分别存入全局 list 中 total_feat_in.append(input) model = Model() modules = model.named_children() # for name, module in modules: module.register_forward_hook(hook_fn_forward) # module.register_backward_hook(hook_fn_backward) # 注意下面代码中 x 的维度,对于linear module,输入一定是大于等于二维的 # (第一维是 batch size)。 x = torch.Tensor([[1.0, 1.0, 1.0]]).requires_grad_() o = model(x) o.backward() print(\'==========Saved inputs and outputs==========\') for idx in range(len(total_feat_in)): print(\'input: \', total_feat_in[idx]) print(\'output: \', total_feat_out[idx])
nn.Module.register_backward_hook(hook_fn)
和register_forward_hook相似,register_backward_hook 的作用是获取神经网络反向传播过程中,各个模块输入端和输出端的梯度值。其中hook_fn的函数签名为:
hook_fn(module, grad_input, grad_output) -> Tensor or None
它的输入变量分别为:模块,模块输入端的梯度,模块输出端的梯度。需要注意的是,这里的输入端和输出端,是站在前向传播的角度的,而不是反向传播的角度。例如线性模块:o=W*x+b,其输入端为 W,x 和 b,输出端为 o。
如果模块有多个输入或者输出的话,grad_input和grad_output可以是 tuple 类型。对于线性模块:o=W*x+b ,它的输入端包括了W、x 和 b 三部分,因此 grad_input 就是一个包含三个元素的 tuple。
这里注意和 forward hook 的不同:
1.在 forward hook 中,input 是 x,而不包括 W 和 b。而 backward hook 的 input 则是 b, W, x 三者的梯度。
2.返回 Tensor 或者 None,backward hook 函数不能直接改变它的输入变量,但是可以返回新的 grad_input,反向传播到它上一个模块。
下面是例子:
import torch from torch import nn class Model(nn.Module): def __init__(self): super(Model, self).__init__() self.fc1 = nn.Linear(3, 4) self.relu1 = nn.ReLU() self.fc2 = nn.Linear(4, 1) self.initialize() def initialize(self): with torch.no_grad(): self.fc1.weight = torch.nn.Parameter( torch.Tensor([[1., 2., 3.], [-4., -5., -6.], [7., 8., 9.], [-10., -11., -12.]])) self.fc1.bias = torch.nn.Parameter(torch.Tensor([1.0, 2.0, 3.0, 4.0])) self.fc2.weight = torch.nn.Parameter(torch.Tensor([[1 0.0, 20.0, 30.0, 40.0]])) self.fc2.bias = torch.nn.Parameter(torch.Tensor([2.0])) def forward(self, x): o = self.fc1(x) o = self.relu1(o) o = self.fc2(o) return o total_grad_out = [] total_grad_in = [] def hook_fn_backward(module, grad_input, grad_output): print(module) # 为了区分模块 # 为了符合反向传播的顺序,我们先打印 grad_output print(\'grad_output\', grad_output) # 再打印 grad_input print(\'grad_input\', grad_input) # 保存到全局变量 total_grad_in.append(grad_input) total_grad_out.append(grad_output) model = Model() modules = model.named_children() for name, module in modules: module.register_backward_hook(hook_fn_backward) # 这里的 requires_grad 很重要,如果不加,backward hook 执行到第一层,对 x 的导数将为 None, # 此外再强调一遍 x 的维度,一定不能写成 torch.Tensor([1.0, 1.0, 1.0]).requires_grad_() 否则 backward hook 会出问题。 x = torch.Tensor([[1.0, 1.0, 1.0]]).requires_grad_() o = model(x) o.backward() print(\'==========Saved inputs and outputs==========\') for idx in range(len(total_grad_in)): print(\'grad output: \', total_grad_out[idx]) print(\'grad input: \', total_grad_in[idx])
输出:
----------------------分割线----------------------- Linear(in_features=4, out_features=1, bias=True) grad_output (tensor([[1.]]),) grad_input (tensor([1.]), tensor([[10., 20., 30., 40.]]), tensor([[ 7.], [ 0.], [27.], [ 0.]])) ReLU() grad_output (tensor([[10., 20., 30., 40.]]),) grad_input (tensor([[10., 0., 30., 0.]]),) Linear(in_features=3, out_features=4, bias=True) grad_output (tensor([[10., 0., 30., 0.]]),) grad_input (tensor([10., 0., 30., 0.]), tensor([[220., 260., 300.]]), tensor([[10., 0., 30., 0.], [10., 0., 30., 0.], [10., 0., 30., 0.]])) ==========Saved inputs and outputs========== grad input: (tensor([1.]), tensor([[10., 20., 30., 40.]]), tensor([[ 7.], [ 0.], [27.], [ 0.]])) grad input: (tensor([[10., 0., 30., 0.]]),) grad input: (tensor([10., 0., 30., 0.]), tensor([[220., 260., 300.]]), tensor([[10., 0., 30., 0.], [10., 0., 30., 0.], [10., 0., 30., 0.]]))
设z=x*W1+b1,c=ReLu(z), y=c*W2+b2。可以根据链式求导法则自行验证梯度。
backward hook 是按反向传播顺序调用模块对应的hook,这里要注意一下。所以结果是先打印fc2、再relu1、最后fc1。ReLu函数的导数(输入>0?1:0)
以上是关于pytorch hook 钩子的主要内容,如果未能解决你的问题,请参考以下文章
--hook,某电商(bao)app签名实现数据采集,欢迎交流