[深入浅出PyTorch]第七章

网络结构可视化

Keras中可以调用一个叫做model.summary()的API来很方便地可视化,调用后就会显示我们的模型参数,输入大小,输出大小,模型的整体参数等,在PyTorch中可以使用print(单纯的print,只能得出基础构件的信息,既不能显示出每一层的shape,也不能显示对应参数量的大小)和torchinfo来可视化

# 安装方法一
pip install torchinfo 
# 安装方法二
conda install -c conda-forge torchinfo
# 使用方法,当然也可以print(torchinfo.summary(...))
torchinfo.summary(<model>, <input_size[batch_size,channel,h,w]>)

torchinfo的输出结果包括模块信息(每一层的类型、输出shape和参数量)、模型整体的参数量、模型大小、一次前向或者反向传播需要的内存大小等

torchinfo的输出结果和之前获取中间层特征输出的net.named_parameters()得到的结果完全不一样,有没有办法联动起来呢?还有

CNN卷积层可视化

理解CNN的重要一步是可视化,包括可视化特征是如何提取的提取到的特征的形式以及模型在输入数据上的关注点

CNN卷积核可视化

卷积核在CNN中负责提取特征,可视化卷积核能够帮助人们理解CNN各个层在提取什么样的特征,进而理解模型的工作原理。

Zeiler和Fergus 2013年的paper中就研究了CNN各个层的卷积核的不同,他们发现靠近输入的层提取的特征是相对简单的结构,而靠近输出的层提取的特征就和图中的实体形状相近

可视化卷积核的核心在于特定层的卷积核即特定层的模型权重,可视化卷积核等价于可视化对应的权重矩阵

# 首先加载模型vgg11
model = vgg11(pretrained=True)
# 将model.features.named_children()返回的对象存为字典
# dict(model.features.named_children())['3']就是名为'3'的层
print(dict(model.features.named_children()))
# 把第三层提取出来
# 但是resnet18没有features属性
conv1 = dict(model.features.named_children())['3']
kernel_set = conv1.weight.detach()
num = len(conv1.weight.detach())
print(kernel_set.shape)
for i in range(0,num):
    i_kernel = kernel_set[i]
    plt.figure(figsize=(20, 17))
    if (len(i_kernel)) > 1:
        for idx, filer in enumerate(i_kernel):
            plt.subplot(9, 9, idx+1) 
            plt.axis('off')
            plt.imshow(filer[ :, :].detach(),cmap='bwr')

第“3”层的特征图由64维变为128维,因此共有128*64个卷积核

CNN特征图可视化方法

输入的原始图像经过每次卷积层得到的数据称为特征图,可视化特征图则是为了看模型提取到的特征是什么样子的(更加直观)

  1. 可以从输入开始,逐层做前向传播,直到想要的特征图处将其返回

  2. 使用hook:pytorch在每一次运算结束后,会将中间变量释放,以节省内存空间,这些会被释放的变量包括非叶子张量的梯度,中间层的特征图等。如果需要提取这些数据,就可以用到hook。pytorch中一共有四个hook,一个应用于tensor,另外三个是针对nn.Module的。

    (hook)相当于插件,可以实现一些额外的功能,而又不用修改主体代码。把这些额外功能实现了挂在主代码上,所以叫钩子,很形象。
    在pytorch中,主题是forward和backward,额外的功能就是对模型的变量进行操作,如“提取”特征图,“提取”非叶子张量的梯度,修改张量梯度等等。

    1. torch.Tensor.register_hook(hook)

      功能:注册一个反向传播hook函数,计算tensor梯度时自动运行;在hook函数中可对梯度grad进行in-place操作,即可修改tensor的grad值

      形式:.register_hook()

      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) # 提取非叶子张量y的梯度
      z = torch.mean(y*y)
      z.backward()
      print("type(y): ", type(y)) # >>> ('type(y): ', )
      print("y.grad: ", y.grad) # >>> ('y.grad: ', None)
      # y是非叶子结点张量,在z.backward()完成之后,y的梯度被释放掉以节省内存
      print("y_grad[0]: ", y_grad[0])
      # >>> ('y_grad[0]: ', tensor([[1.0000, 1.5000],[2.0000, 2.5000]]))
      y.remove()
      
      def grad_hook(grad):
          grad *= 2
      x = torch.tensor([2., 2., 2., 2.], requires_grad=True)
      y = torch.pow(x, 2)
      z = torch.mean(y)
      h = x.register_hook(grad_hook)
      z.backward()
      print(x.grad)
      h.remove()    # removes the hook
      # >>> tensor([2., 2., 2., 2.])
      
    2. torch.nn.Module.register_forward_hook

      功能:提取特征图

      数据通过网络向前传播,网络某一层我们预先设置了一个钩子,数据传播过后钩子上会留下数据在这一层的样子,读取钩子的信息就是这一层的特征图

      class Hook(object):
          def __init__(self):
              self.module_name = []
              self.features_in_hook = []
              self.features_out_hook = []
      
          def __call__(self,module, fea_in, fea_out):
              print("hooker working", self)
              self.module_name.append(module.__class__)
              self.features_in_hook.append(fea_in)
              self.features_out_hook.append(fea_out)
              return None
        
      
      def plot_feature(model, idx):
          hh = Hook()
          model.features[idx].register_forward_hook(hh)
        
          forward_model(model,False)
          print(hh.module_name)
          print((hh.features_in_hook[0][0].shape))
          print((hh.features_out_hook[0].shape))
        
          out1 = hh.features_out_hook[0]
      
          total_ft  = out1.shape[1]
          first_item = out1[0].cpu().clone()  
      
          plt.figure(figsize=(20, 17))
        
      
          for ftidx in range(total_ft):
              if ftidx > 99:
                  break
              ft = first_item[ftidx]
              plt.subplot(10, 10, ftidx+1) 
        
              plt.axis('off')
              #plt.imshow(ft[ :, :].detach(),cmap='gray')
              plt.imshow(ft[ :, :].detach())
      

      首先实现了一个hook类,之后在plot_feature函数中,将该hook类的对象注册到要进行可视化的网络的某层中。model在进行前向传播的时候会调用hook__call__函数,我们也就是在那里存储了当前层的输入和输出。这里的features_out_hook是一个list,每次前向传播一次,都是调用一次,也就是features_out_hook 长度会增加1。

    3. torch.nn.Module.register_forward_pre_hook

    4. torch.nn.Module.register_backward_hook

class activation map可视化方法

class activation map(CAM)的作用是判断哪些变量对模型来说是重要的,在CNN可视化的场景下,即判断图像中哪些像素点对预测结果是重要的。除了确定重要的像素点,人们也会对重要区域的梯度感兴趣,因此在CAM的基础上也进一步改进得到了Grad-CAM(以及诸多变种)

通过pip install grad-cam进行安装

import torch
from torchvision.models import vgg11,resnet18,resnet101,resnext101_32x8d
import matplotlib.pyplot as plt
from PIL import Image
import numpy as np

model = vgg11(pretrained=True)
img_path = './dog.jpg'
# resize操作是为了和传入神经网络训练图片大小一致
img = Image.open(img_path).resize((224,224))
# 需要将原始图片转为np.float32格式并且在0-1之间 
rgb_img = np.float32(img)/255
plt.imshow(img)
from pytorch_grad_cam import GradCAM,ScoreCAM,GradCAMPlusPlus,AblationCAM,XGradCAM,EigenCAM,FullGrad
from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget
from pytorch_grad_cam.utils.image import show_cam_on_image

target_layers = [model.features[-1]]
# 选取合适的类激活图,但是ScoreCAM和AblationCAM需要batch_size
cam = GradCAM(model=model,target_layers=target_layers)
targets = [ClassifierOutputTarget(preds)]   
# 上方preds需要设定,比如ImageNet有1000类,这里可以设为200
grayscale_cam = cam(input_tensor=img_tensor, targets=targets)
grayscale_cam = grayscale_cam[0, :]
cam_img = show_cam_on_image(rgb_img, grayscale_cam, use_rgb=True)
print(type(cam_img))
Image.fromarray(cam_img)
使用FlashTorch快速实现CNN可视化

通过pip install flashtorch进行安装

  1. 可视化梯度

  2. 可视化卷积核

使用TensorBoard可视化训练过程

联合阅读((20220204193821-stopmsu ‘tensorboard’))

一般来说,我们希望可视化训练集的损失函数和验证集的损失函数,绘制两条损失函数的曲线来确定训练的终点;此外,我们也希望可视化其他内容,如输入数据(尤其是图片)、模型结构、参数分布等,这些对于我们在debug中查找问题来源非常重要(比如输入数据和我们想象的是否一致)

安装:pip install tensorboardX,from tensorboardX import SummaryWriter或者也可以使用pytorch自带的tensorboard,则使用from torch.utils.tensorboard import SummaryWriter

配置与启动

在使用TensorBoard前,我们需要先指定一个文件夹供TensorBoard保存记录下来的数据。然后调用tensorboard中的SummaryWriter作为上述"记录员"

from tensorboardX import SummaryWriter
# from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter('./runs')

上面的操作实例化SummaryWritter为变量writer,并指定writer的输出目录为当前目录下的"runs"目录,可以手动往runs文件夹里添加数据用于可视化,或者把runs文件夹里的数据放到其他机器上可视化。

在命令行输入tensorboard --logdir=/path/to/logs/ --port=xxxx以启动tensorboard

其中"path/to/logs/"是指定的保存tensorboard记录结果的文件路径,port是外部访问TensorBoard的端口号,可以通过访问ip:port访问tensorboard

为了tensorboard能够不断地在后台运行,也可以使用nohup命令或者tmux工具来运行tensorboard

可视化模型结构
writer.add_graph(model, input_to_model = torch.rand(<input_size>))
writer.close()
图像可视化

对于单张图片的显示使用add_image,对于多张图片的显示使用add_images,也可以使用torchvision.utils.make_grid将多张图片拼成一张图片后,用writer.add_iamge显示

以CIFAR10数据集为例:

import torchvision
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

transform_train = transforms.Compose(
    [transforms.ToTensor()])
transform_test = transforms.Compose(
    [transforms.ToTensor()])

train_data = datasets.CIFAR10(".", train=True, download=True, transform=transform_train)
test_data = datasets.CIFAR10(".", train=False, download=True, transform=transform_test)
train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
test_loader = DataLoader(test_data, batch_size=64)

images, labels = next(iter(train_loader))
# 仅查看一张图片
writer = SummaryWriter('./pytorch_tb')
writer.add_image('images[0]', images[0])
writer.close()
# 将多张图片拼接成一张图片,中间用黑色网格分割
# create grid of images
writer = SummaryWriter('./pytorch_tb')
img_grid = torchvision.utils.make_grid(images)
writer.add_image('image_grid', img_grid)
writer.close()
# 将多张图片直接写入
writer = SummaryWriter('./pytorch_tb')
writer.add_images("images",images,global_step = 0)
writer.close()

除了可视化原始图像,TensorBoard提供的可视化方案也适用于我们在Python中用matplotlib等工具绘制的其他图像,用于展示分析结果等内容

Tensorboard连续变量可视化

可视化连续变量(或时序变量)的变化过程,通过add_scalar实现

writer = SummaryWriter('./pytorch_tb')
for i in range(500):
    x = i
    y = x**2
    writer.add_scalar("x", x, i) #日志中记录x在第step i 的值
    writer.add_scalar("y", y, i) #日志中记录y在第step i 的值
writer.close()

如果需要在同一张图显示多个曲线,则:

  1. 分别建立存放的子路径
  2. add_scalar中使曲线的tag保持一致
writer1 = SummaryWriter('./pytorch_tb/x')
writer2 = SummaryWriter('./pytorch_tb/y')
for i in range(500):
    x = i
    y = x*2
    writer1.add_scalar("same", x, i) #日志中记录x在第step i 的值
    writer2.add_scalar("same", y, i) #日志中记录y在第step i 的值
writer1.close()
writer2.close()

你可能感兴趣的:(python学习,python)