【深度之眼】Pytorch框架班第五期-Hook函数与CAM算法代码调试解析

Hook函数概念

Hook函数机制:不改变主体,实现额外功能,像一个挂件,挂钩,hook。由于pytorch采用的计算图为动态图,动态图运算结束后,产生的中间变量(例如特征图,非叶子节点的梯度)会被释放掉,但是往往有时候我们想要提取这些中间变量,这时我们就可以采用Hook函数。

import torch
import torch.nn as nn

import sys, os
hello_pytorch_DIR = os.path.abspath(os.path.dirname(__file__)+os.path.sep+".."+os.path.sep+"..")
sys.path.append(hello_pytorch_DIR)

from tools.common_tools import set_seed

set_seed(1)  # 设置随机种子

1、Tensor.register_hook

hook(grad) -> Tensor or None

功能: 注册一个反向传播hook函数。因为张量在反向传播时非叶子节点的梯度会消失。
Hook函数仅一个输入参数,为张量的梯度。
在这里插入图片描述

查看梯度

在前向传播和反向传播后非叶子节点a和b的梯度会被释放,下面我们使用hook函数捕获a张量的梯度。

# ----------------------------------- 1 tensor hook 1 -----------------------------------
# flag = 0
flag = 1
if flag:

    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):
        a_grad.append(grad)

    handle = a.register_hook(grad_hook)

    y.backward()

    # 查看梯度
    print("gradient:", w.grad, x.grad, a.grad, b.grad, y.grad)
    print("a_grad[0]: ", a_grad[0])
    handle.remove()

在这里插入图片描述

修改叶子节点的梯度

# ----------------------------------- 2 tensor hook 2 -----------------------------------
# flag = 0
flag = 1
if flag:

    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()

在这里插入图片描述
当grad_hook函数带return的时候,我们可以得到下面的结果,我们发现return回去的张量会覆盖掉我们return回去的张量的梯度。

在这里插入图片描述

2、Module.register_forward_hook

hook(module, input, output) -> None

功能:注册module的前向传播hook函数
module:当前网络层
input:当前网络层输入数据
output:当前网络层输出数据
在这里插入图片描述
我们通过Module.register_forward_hook函数来获取中间的特征图feature_map。

# ----------------------------------- 3 Module.register_forward_hook and pre hook -----------------------------------
# flag = 0
flag = 1
if flag:

    class Net(nn.Module):
        def __init__(self):
            super(Net, self).__init__()
            self.conv1 = nn.Conv2d(1, 2, 3)
            self.pool1 = nn.MaxPool2d(2, 2)

        def forward(self, x):
            x = self.conv1(x)
            x = self.pool1(x)
            return x
	
	# 模型,模型的输入,模型的输出
    def forward_hook(module, data_input, data_output):
        fmap_block.append(data_output)
        input_block.append(data_input)

    def forward_pre_hook(module, data_input):
        print("forward_pre_hook input:{}".format(data_input))

    def backward_hook(module, grad_input, grad_output):
        print("backward hook input:{}".format(grad_input))
        print("backward hook output:{}".format(grad_output))

    # 初始化网络
    net = Net()
    net.conv1.weight[0].detach().fill_(1)
    net.conv1.weight[1].detach().fill_(2)
    net.conv1.bias.data.detach().zero_()

    # 注册hook
    fmap_block = list()
    input_block = list()
    net.conv1.register_forward_hook(forward_hook)
    # net.conv1.register_forward_pre_hook(forward_pre_hook)
    # net.conv1.register_backward_hook(backward_hook)

    # inference
    fake_img = torch.ones((1, 1, 4, 4))   # batch size * channel * H * W
    output = net(fake_img)

    # loss_fnc = nn.L1Loss()
    # target = torch.randn_like(output)
    # loss = loss_fnc(target, output)
    # loss.backward()

    # 观察
    print("output shape: {}\noutput value: {}\n".format(output.shape, output))
    print("feature maps shape: {}\noutput value: {}\n".format(fmap_block[0].shape, fmap_block[0]))
    print("input shape: {}\ninput value: {}".format(input_block[0][0].shape, input_block[0]))


在这里插入图片描述

单步调试

在114行设置断点进行单步调试,
在这里插入图片描述
我们通过step into进入到module.py文件中Module类中的__call__函数中,该函数主要分为4个部分,首先进行_forward_pre_hooks的判断,然后执行一个forward函数,第三部分执行_forward_hook,所以forward_hook实在这里实现的,最后一个部分时_backward_hooks。所以整个__call__函数可以拆分为4个部分。由于我们是对卷积层执行了一个forward_hook,所以在卷积层执行完forward函数之后,才进入到我我们定义的forward_hook函数中。
在这里插入图片描述
接下来我们进入卷积层的forward函数中,将代码运行到541处,然后点击step into。下图可知我们来到了net的forward中,

在这里插入图片描述
继续step into,此时我们进入到卷积层的__call__函数中,
在这里插入图片描述
此时self中的_forward_pre_hook字典为空,而_forward_hook不为空,
在这里插入图片描述

所以我们先运行卷积层的forward函数,然后我们得到的result为特征图,然后再判断forward_hook,由于forward_hook存在,因此我们进入到下面的for循环中,hook函数接受三个变量第一个为module即网络层,第二个变量为输入,第三个变量为输出。
在这里插入图片描述
此时点击step into跳入该hook函数,我们可以发现程序跳入到了我们自定义的forward_hook函数中,进行我们需要的处理。
在这里插入图片描述

3、Module.register_forward_pre_hook

hook(module, input) -> None

功能:注册module前向传播的hook函数,可以查看网络层之前的数据。
参数:

  • module:当前网络层
  • input:当前网络层输入数据

4、Module.register_backward_hook

hook(module, grad_input, grad_output) -> Tensor or None

功能:注册module反向传播前的hook函数
参数:

  • module:当前网络层
  • grad_input:当前网络层输入梯度数据
  • grad_output: 当前网络层输出梯度数据

接下来我们通过将调用两个函数的代码解除注释来观察运行的结果。
在这里插入图片描述

采用Hook函数来获取卷积神经网络中的特征图,并进行可视化(AlexNet)

import torch.nn as nn
import numpy as np
from PIL import Image
import torchvision.transforms as transforms
import torchvision.utils as vutils
from torch.utils.tensorboard import SummaryWriter
import os
import sys
hello_pytorch_DIR = os.path.abspath(os.path.dirname(__file__)+os.path.sep+".."+os.path.sep+"..")
sys.path.append(hello_pytorch_DIR)

from tools.common_tools import set_seed
import torchvision.models as models

set_seed(1)  # 设置随机种子

# ----------------------------------- feature map visualization -----------------------------------
# flag = 0
flag = 1
if flag:
    writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")

    # 数据
    path_img = "./lena.png"     # your path to image
    normMean = [0.49139968, 0.48215827, 0.44653124]
    normStd = [0.24703233, 0.24348505, 0.26158768]

    norm_transform = transforms.Normalize(normMean, normStd)
    img_transforms = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        norm_transform
    ])

    img_pil = Image.open(path_img).convert('RGB')
    if img_transforms is not None:
        img_tensor = img_transforms(img_pil)
    img_tensor.unsqueeze_(0)    # chw --> bchw

    # 模型
    alexnet = models.alexnet(pretrained=True)

    # 注册hook
    fmap_dict = dict()
    for name, sub_module in alexnet.named_modules():

        if isinstance(sub_module, nn.Conv2d):
            key_name = str(sub_module.weight.shape)
            fmap_dict.setdefault(key_name, list())

            n1, n2 = name.split(".")

            def hook_func(m, i, o):
                key_name = str(m.weight.shape)
                fmap_dict[key_name].append(o)

            alexnet._modules[n1]._modules[n2].register_forward_hook(hook_func)

    # forward
    output = alexnet(img_tensor)

    # add image
    for layer_name, fmap_list in fmap_dict.items():
        fmap = fmap_list[0]
        fmap.transpose_(0, 1)

        nrow = int(np.sqrt(fmap.shape[0]))
        fmap_grid = vutils.make_grid(fmap, normalize=True, scale_each=True, nrow=nrow)
        writer.add_image('feature map in {}'.format(layer_name), fmap_grid, global_step=322)


运行上述代码,并用tensorboard打开显示效果。
在这里插入图片描述

CAM类激活图

在这里插入图片描述
CAM对网络最后一个特征图进行加权求和得到一个注意力的机制,可以知道我们的网络更关注什么地方。CAM对特征图进行加权平均,权值w通过global average pooling(全局平均池化),一张图对应一个神经元,然后经过全连接层得到网络的输出。图像输出的类所对应的神经元的权重即为w。
CAM的缺点为,网络最后一层的特征图要经过全局平均池化才能得到权值,所以CAM并不是很实用,因为我们分析一个网络还要改动它后面网络层再重新训练。
在这里插入图片描述
CAM中最重要的时特征图和特征图对应的权重,Grad_CAM 采用梯度作为特征图权重。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值