8.4 hook函数与CAM可视化

    科技2022-07-15  112

    这节课学习hook函数,用hook函数来提取特征图,进行可视化。

    然后介绍CAM算法。

     

    一、Hook函数的概念

    二、Hook函数

    三、特征图可视化

    四、CAM(class activation map, 类激活图)

     

     

    一、Hook函数的概念

    hook函数不改变主题,实现额外功能。hook的翻译是钩子。

    在pytorch中,采用动态图机制,运算结束之后,一些中间变量会被释放掉。例如特征图、非叶子节点的梯度。但是有时候,我们想要再提取这些中间变量。这时候,我们就可以使用hook函数。

     

    二、Hook函数

    在pytorch中提供了4种hook函数。

     

    其中第一个是针对Tensor的,剩余三个是针对module的。

     

     

    1. Tensor.register_hook()

    功能是注册一个反向传播的hook函数。因为张量只有在反向传播的时候,非叶子节点的梯度才会消失,所以才有了这个hook函数。

     

    这个hook函数只接受一个参数,就是张量的梯度。输出可以是一个张量,也可以是None。如果输出为张量的话,就会覆盖当前张量的梯度;如果输出为None的话,就不会改变梯度。

     

    下面我们结合之前的这个计算图,来学习:

    计算图如下:

    a = x+w

    b = w+1

    y = a*b 

     

    y对a的梯度= b = w+1 = 2

    y对b的梯度=a = x+w = 3

    y对w的梯度=b*1 + a*1 = a+b = 5. 

    我们知道当反向传播结束后,y对a和b的导数就被释放掉。我们使用hook()函数记录下来。

     

    例1:保存非叶子节点的梯度信息。

    # -*- coding:utf-8 -*- """ @file name : hook_methods.py @brief : 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) # 设置随机种子 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() #用来保存a的梯度 def grad_hook(grad): #定义钩子函数:传入一个参数grad a_grad.append(grad) #将这个梯度保存一份 handle = a.register_hook(grad_hook) #注册hook函数:grad_hook()。register_hook()方法接受一个函数。 y.backward() #反向传播。当执行完之后a的梯度会被释放掉。 # 查看梯度 print("gradient:", w.grad, x.grad, a.grad, b.grad, y.grad) #a、b、y三个非叶子节点的梯度应该消失了。 print("a_grad[0]: ", a_grad[0]) #可以看到保存的梯度信息。 handle.remove()

    运行结果:

    我们在函数主体backward()上挂上了一个额外的方法,grad_hook(),对于张量a,增加了功能,把它的梯度保存了下来。

    所以当backward()结束之后,a的梯度也就保存了下来。

     

    例2:修改梯度信息

    # -*- coding:utf-8 -*- """ @file name : hook_methods.py @brief : 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) # 设置随机种子 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 handle = a.register_hook(grad_hook) y.backward() # 查看梯度 print("a.grad: ", a.grad) print("w.grad:", w.grad) handle.remove()

    结果:

    因为在hook函数中没有保存a的梯度,当反向传播函数执行完之后,就释放掉了,所以为None.

    正常来说,y对w的梯度=b*1 + a*1 = a+b = 2+3 = 5。但是在反向传播的过程中,a的梯度经过hook()函数时变成了4,所以反向传播结束后y对w的梯度变成了4+3 = 7.

    把hook()函数改为以下内容,也会得到同样的结果:

    返回的是一个张量,会覆盖掉原始张量的梯度。

     

    2. Module.register_forward_hook()

    通常我们使用forward_hook()函数来获取卷积输出的特征图。

    在前向传播的时候,特征图会释放掉。我们就可以使用这个函数来获取特征图。

     

    例:我们来构建右图中的网络,然后获取特征图。

    # -*- coding:utf-8 -*- """ @file name : hook_methods.py # @author : TingsongYu https://github.com/TingsongYu @date : 2019-10-28 @brief : 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) # 设置随机种子 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): #定义hook(), fmap_block.append(data_output) input_block.append(data_input) # 初始化网络 net = Net() net.conv1.weight[0].detach().fill_(1) #对网络的权值进行初始化。第一个卷积核所有权重都是1 net.conv1.weight[1].detach().fill_(2) #第二个卷积核所有权重都是2 net.conv1.bias.data.detach().zero_() #bias全为0 # 注册hook fmap_block = list() input_block = list() net.conv1.register_forward_hook(forward_hook) #对第一个卷积层注册hook() # inference fake_img = torch.ones((1, 1, 4, 4)) # batch size * channel * H * W output = net(fake_img) #前向传播 #观察 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]))

    结果:

     

    通过单步调试,理解hook()函数是怎么实现的。运行机制。

    可以看到前向传播的call()函数是分为4部分的。首先会执行 pre_hooks(),  然后执行forward(), 再执行forward_hooks,最后执行backward_hooks。

     

    3. register_forward_pre_hook()

    这个hook函数可以查看网络层之前的数据。还没开始运行forward()。

     

    例:

    # -*- coding:utf-8 -*- """ @file name : hook_methods.py @brief : 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) # 设置随机种子 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_pre_hook(module, data_input): print("forward_pre_hook input:{}".format(data_input)) # 初始化网络 net = Net() net.conv1.weight[0].detach().fill_(1) net.conv1.weight[1].detach().fill_(2) net.conv1.bias.data.detach().zero_() # 注册hook net.conv1.register_forward_pre_hook(forward_pre_hook) # inference fake_img = torch.ones((1, 1, 4, 4)) # batch size * channel * H * W output = net(fake_img)

    运行结果:

     

     

    4. regigster_backward_hook()

     

    例:

    # -*- coding:utf-8 -*- """ @file name : hook_methods.py # @author : TingsongYu https://github.com/TingsongYu @date : 2019-10-28 @brief : 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) # 设置随机种子 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 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 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) #因为要执行backward_hook,所以要给一个损失,让其反向传播 loss_fnc = nn.L1Loss() target = torch.randn_like(output) loss = loss_fnc(target, output) loss.backward()

    结果:

     

    三、特征图的可视化

    8.3中讲解的特征图可视化,是获取了网络中的一层,然后手动实现一个forward。这样做的效率是很低的,我们可以使用hook函数来实现。

    # -*- coding:utf-8 -*- """ @file name : hook_fmap_vis.py @brief : 采用hook函数可视化特征图 """ import torch.nn as nn import torch 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 ----------------------------------- 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) #如果这么写的话,会不用写后面四句了,但是会先下载预训练模型,很慢。 path_state_dict = os.path.join("./alexnet-owt-4df8aa71.pth") alexnet = models.alexnet() pretrained_state_dict = torch.load(path_state_dict) #以下两句加载预训练模型。 alexnet.load_state_dict(pretrained_state_dict) # 注册hook fmap_dict = dict() #定义字典,用来存储所有卷积层的特征图 for name, sub_module in alexnet.named_modules(): #alexnet.named_modules()返回所有的子网络层。 if isinstance(sub_module, nn.Conv2d): #如果是一个卷积层,就给它注册一个forward_hook(),把它的特征图记录下来。 key_name = str(sub_module.weight.shape) fmap_dict.setdefault(key_name, list()) #注册一个<key,value>对 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) #注册hook() # forward output = alexnet(img_tensor) # add image。以eventfile形式存储到硬盘中。 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)

     

    结果:

    后面的特征图看不出来。

     

    四、CAM(class activation map, 类激活图)

    下面来学习一个很实用的可视化方法。

    1. CAM

    类激活图。这个算法用来分析卷积神经网络。CNN得到一个输出,那么我们的网络是关注哪些部分而得到这个输出?比如一张图片输入网络,最后输出认为是一个澳大利亚的犬种。为什么会分为这一类呢?网络看到了什么呢?

    CAM的基本思想是:会对网络的最后一个特征图进行加权求和。具体的操作就是下面这样,比如最后一个由n个特征图,每一个会乘以权值,相加会得到最终的CAM。这个map中越红的地方权重越大,值越大。

    CAM怎么获取特征图的权重?首先对特征图做一个GAM(全局平均池化),一张图对应一个神经元。然后接一个全连接层,就可以得到网络输出。构建好之后,输出对应的类所对应的权值就是特征图的权重。

     

    2. Grad-CAM

    CAM有一个缺点就是,必须要有一个GAP的操作,才能得到权值。还要改一下网络,再训练。

    针对这个弊端,又有新的算法提出:Grad-CAM。

    利用梯度作为特征图的权重。

    Grad-CAM,更有普适性。

    下面介绍一个有意思的实验:

     

    可以看到,并不是真的看到是飞机,认为是飞机。。。

     

     

     

    Processed: 0.011, SQL: 8