【深入浅出PyTorch】--7.1.PyTorch可视化1
随着深度神经网络做的的发展,网络的结构越来越复杂,我们也很难确定每一层的输入结构,输出结构以及参数等信息,这样导致我们很难在短时间内完成debug。因此掌握一个可以用来可视化网络结构的工具是十分有必要的。类似的功能在另一个深度学习库Keras中可以调用一个叫做model.summary()
的API来很方便地实现,调用后就会显示我们的模型参数,输入大小,输出大小,模型的整体参数等,但是在PyTorch中没有这样一种便利的工具帮助我们可视化我们的模型结构。
为了解决这个问题,人们开发了torchinfo工具包 ( torchinfo是由torchsummary和torchsummaryX重构出的库) 。本节我们将介绍如何使用torchinfo来可视化网络结构。
经过本节的学习,你将收获:
-
可视化网络结构的方法
目录
1.可视化网络结构
1.2.print直接打印
1.2.torchinfo可视化
2.CNN可视化
2.1.CNN卷积核可视化
2.2.CNN特征图可视化--Hook
2.3.CNN可视化方法--CAM
2.4.CNN可视化快速实现--FlashTorch
1.可视化网络结构
1.2.print直接打印
在本节中,我们将使用ResNet18的结构进行展示。
import torchvision.models as models
model = models.resnet18()
print(model)
通过上面的两步,我们就得到resnet18的模型结构。在学习torchinfo之前,让我们先看下直接print(model)的结果。
ResNet((conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)(relu): ReLU(inplace=True)(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)(layer1): Sequential((0): Bottleneck((conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)(relu): ReLU(inplace=True)(downsample): Sequential((0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)))... ...)(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))(fc): Linear(in_features=2048, out_features=1000, bias=True) )
我们可以发现单纯的print(model),只能得出基础构件的信息,既不能显示出每一层的shape,也不能显示对应参数量的大小,为了解决这些问题,我们就需要介绍出我们今天的主人公torchinfo
。
1.2.torchinfo可视化
-
torchinfo的安装
# 安装方法一
pip install torchinfo
# 安装方法二
conda install -c conda-forge torchinfo
-
torchinfo的使用
trochinfo的使用也是十分简单,我们只需要使用torchinfo.summary()
就行了,必需的参数分别是model,input_size[batch_size,channel,h,w],更多参数可以参考documentation,下面让我们一起通过一个实例进行学习。
import torchvision.models as models
from torchinfo import summary
resnet18 = models.resnet18() # 实例化模型
summary(resnet18, (1, 3, 224, 224)) # 1:batch_size 3:图片的通道数 224: 图片的高宽
-
torchinfo的结构化输出
=========================================================================================
Layer (type:depth-idx) Output Shape Param #
=========================================================================================
ResNet -- --
├─Conv2d: 1-1 [1, 64, 112, 112] 9,408
├─BatchNorm2d: 1-2 [1, 64, 112, 112] 128
├─ReLU: 1-3 [1, 64, 112, 112] --
├─MaxPool2d: 1-4 [1, 64, 56, 56] --
├─Sequential: 1-5 [1, 64, 56, 56] --
│ └─BasicBlock: 2-1 [1, 64, 56, 56] --
│ │ └─Conv2d: 3-1 [1, 64, 56, 56] 36,864
│ │ └─BatchNorm2d: 3-2 [1, 64, 56, 56] 128
│ │ └─ReLU: 3-3 [1, 64, 56, 56] --
│ │ └─Conv2d: 3-4 [1, 64, 56, 56] 36,864
│ │ └─BatchNorm2d: 3-5 [1, 64, 56, 56] 128
│ │ └─ReLU: 3-6 [1, 64, 56, 56] --
│ └─BasicBlock: 2-2 [1, 64, 56, 56] --
│ │ └─Conv2d: 3-7 [1, 64, 56, 56] 36,864
│ │ └─BatchNorm2d: 3-8 [1, 64, 56, 56] 128
│ │ └─ReLU: 3-9 [1, 64, 56, 56] --
│ │ └─Conv2d: 3-10 [1, 64, 56, 56] 36,864
│ │ └─BatchNorm2d: 3-11 [1, 64, 56, 56] 128
│ │ └─ReLU: 3-12 [1, 64, 56, 56] --
├─Sequential: 1-6 [1, 128, 28, 28] --
│ └─BasicBlock: 2-3 [1, 128, 28, 28] --
│ │ └─Conv2d: 3-13 [1, 128, 28, 28] 73,728
│ │ └─BatchNorm2d: 3-14 [1, 128, 28, 28] 256
│ │ └─ReLU: 3-15 [1, 128, 28, 28] --
│ │ └─Conv2d: 3-16 [1, 128, 28, 28] 147,456
│ │ └─BatchNorm2d: 3-17 [1, 128, 28, 28] 256
│ │ └─Sequential: 3-18 [1, 128, 28, 28] 8,448
│ │ └─ReLU: 3-19 [1, 128, 28, 28] --
│ └─BasicBlock: 2-4 [1, 128, 28, 28] --
│ │ └─Conv2d: 3-20 [1, 128, 28, 28] 147,456
│ │ └─BatchNorm2d: 3-21 [1, 128, 28, 28] 256
│ │ └─ReLU: 3-22 [1, 128, 28, 28] --
│ │ └─Conv2d: 3-23 [1, 128, 28, 28] 147,456
│ │ └─BatchNorm2d: 3-24 [1, 128, 28, 28] 256
│ │ └─ReLU: 3-25 [1, 128, 28, 28] --
├─Sequential: 1-7 [1, 256, 14, 14] --
│ └─BasicBlock: 2-5 [1, 256, 14, 14] --
│ │ └─Conv2d: 3-26 [1, 256, 14, 14] 294,912
│ │ └─BatchNorm2d: 3-27 [1, 256, 14, 14] 512
│ │ └─ReLU: 3-28 [1, 256, 14, 14] --
│ │ └─Conv2d: 3-29 [1, 256, 14, 14] 589,824
│ │ └─BatchNorm2d: 3-30 [1, 256, 14, 14] 512
│ │ └─Sequential: 3-31 [1, 256, 14, 14] 33,280
│ │ └─ReLU: 3-32 [1, 256, 14, 14] --
│ └─BasicBlock: 2-6 [1, 256, 14, 14] --
│ │ └─Conv2d: 3-33 [1, 256, 14, 14] 589,824
│ │ └─BatchNorm2d: 3-34 [1, 256, 14, 14] 512
│ │ └─ReLU: 3-35 [1, 256, 14, 14] --
│ │ └─Conv2d: 3-36 [1, 256, 14, 14] 589,824
│ │ └─BatchNorm2d: 3-37 [1, 256, 14, 14] 512
│ │ └─ReLU: 3-38 [1, 256, 14, 14] --
├─Sequential: 1-8 [1, 512, 7, 7] --
│ └─BasicBlock: 2-7 [1, 512, 7, 7] --
│ │ └─Conv2d: 3-39 [1, 512, 7, 7] 1,179,648
│ │ └─BatchNorm2d: 3-40 [1, 512, 7, 7] 1,024
│ │ └─ReLU: 3-41 [1, 512, 7, 7] --
│ │ └─Conv2d: 3-42 [1, 512, 7, 7] 2,359,296
│ │ └─BatchNorm2d: 3-43 [1, 512, 7, 7] 1,024
│ │ └─Sequential: 3-44 [1, 512, 7, 7] 132,096
│ │ └─ReLU: 3-45 [1, 512, 7, 7] --
│ └─BasicBlock: 2-8 [1, 512, 7, 7] --
│ │ └─Conv2d: 3-46 [1, 512, 7, 7] 2,359,296
│ │ └─BatchNorm2d: 3-47 [1, 512, 7, 7] 1,024
│ │ └─ReLU: 3-48 [1, 512, 7, 7] --
│ │ └─Conv2d: 3-49 [1, 512, 7, 7] 2,359,296
│ │ └─BatchNorm2d: 3-50 [1, 512, 7, 7] 1,024
│ │ └─ReLU: 3-51 [1, 512, 7, 7] --
├─AdaptiveAvgPool2d: 1-9 [1, 512, 1, 1] --
├─Linear: 1-10 [1, 1000] 513,000
=========================================================================================
Total params: 11,689,512
Trainable params: 11,689,512
Non-trainable params: 0
Total mult-adds (G): 1.81
=========================================================================================
Input size (MB): 0.60
Forward/backward pass size (MB): 39.75
Params size (MB): 46.76
Estimated Total Size (MB): 87.11
=========================================================================================
我们可以看到torchinfo提供了更加详细的信息,包括模块信息(每一层的类型、输出shape和参数量)、模型整体的参数量、模型大小、一次前向或者反向传播需要的内存大小等
注意:
但你使用的是colab或者jupyter notebook时,想要实现该方法,summary()
一定是该单元(即notebook中的cell)的返回值,否则我们就需要使用print(summary(...))
来可视化。
2.CNN可视化
卷积神经网络(CNN)是深度学习中非常重要的模型结构,它广泛地用于图像处理,极大地提升了模型表现,推动了计算机视觉的发展和进步。但CNN是一个“黑盒模型”,人们并不知道CNN是如何获得较好表现的,由此带来了深度学习的可解释性问题。如果能理解CNN工作的方式,人们不仅能够解释所获得的结果,提升模型的鲁棒性,而且还能有针对性地改进CNN的结构以获得进一步的效果提升。
理解CNN的重要一步是可视化,包括可视化特征是如何提取的、提取到的特征的形式以及模型在输入数据上的关注点等。本节我们就从上述三个方面出发,介绍如何在PyTorch的框架下完成CNN模型的可视化。
经过本节的学习,你将收获:
-
可视化CNN卷积核的方法
-
可视化CNN特征图的方法
-
可视化CNN显著图(class activation map)的方法
2.1.CNN卷积核可视化
卷积核在CNN中负责提取特征,可视化卷积核能够帮助人们理解CNN各个层在提取什么样的特征,进而理解模型的工作原理。例如在Zeiler和Fergus 2013年的paper:https://arxiv.org/pdf/1311.2901中就研究了CNN各个层的卷积核的不同,他们发现靠近输入的层提取的特征是相对简单的结构,而靠近输出的层提取的特征就和图中的实体形状相近了,如下图所示:
在PyTorch中可视化卷积核也非常方便,核心在于特定层的卷积核即特定层的模型权重,可视化卷积核就等价于可视化对应的权重矩阵。下面给出在PyTorch中可视化卷积核的实现方案,以torchvision自带的VGG11模型为例。
首先加载模型,并确定模型的层信息:
import torch
from torchvision.models import vgg11model = vgg11(pretrained=True)
print(dict(model.features.named_children()))
{'0': Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),'1': ReLU(inplace=True),'2': MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False),'3': Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),'4': ReLU(inplace=True),'5': MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False),'6': Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),'7': ReLU(inplace=True),'8': Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),'9': ReLU(inplace=True),'10': MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False),'11': Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),'12': ReLU(inplace=True),'13': Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),'14': ReLU(inplace=True),'15': MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False),'16': Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),'17': ReLU(inplace=True),'18': Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),'19': ReLU(inplace=True),'20': MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)}
卷积核对应的应为卷积层(Conv2d),这里以第“3”层为例,可视化对应的参数:
import matplotlib.pyplot as plt
import torchvision%matplotlib inlineconv1 = 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个卷积核,其中部分卷积核可视化效果如下图所示:
2.2.CNN特征图可视化--Hook
与卷积核相对应,输入的原始图像经过每次卷积层得到的数据称为特征图,可视化卷积核是为了看模型提取哪些特征,可视化特征图则是为了看模型提取到的特征是什么样子的。
获取特征图的方法有很多种,可以从输入开始,逐层做前向传播,直到想要的特征图处将其返回。尽管这种方法可行,但是有些麻烦了。在PyTorch中,提供了一个专用的接口使得网络在前向传播过程中能够获取到特征图,这个接口的名称非常形象,叫做hook。可以想象这样的场景,数据通过网络向前传播,网络某一层我们预先设置了一个钩子,数据传播过后钩子上会留下数据在这一层的样子,读取钩子的信息就是这一层的特征图。
🧠 一、Hook 的原理
在 PyTorch 中,torch.nn.Module
提供了注册 前向传播钩子(forward hook) 和 反向传播钩子(backward hook) 的功能。
hook = layer.register_forward_hook(hook_fn)
layer
: 想要监控的网络层(如nn.Conv2d
)hook_fn
: 回调函数,格式为:
def hook_fn(module, input, output):# module: 当前层对象# input: 输入(tuple)# output: 输出(tensor)
- 加载预训练模型(如 VGG、ResNet)
- 注册 hook 到目标卷积层
- 前向传播一张图像
- hook 自动保存特征图
- 可视化特征图
import torch
import torch.nn as nn
import torchvision.models as models
import torchvision.transforms as transforms
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np# 1. 加载预训练模型
model = models.vgg16(pretrained=True)
model.eval() # 设置为评估模式# 2. 定义图像预处理
transform = transforms.Compose([transforms.Resize((224, 224)),transforms.ToTensor(),transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])# 3. 加载并预处理图像(使用随机噪声或真实图像)
# 这里用随机图像演示,你可以换成真实图片
# image = Image.open('your_image.jpg').convert('RGB')
# 或者用随机张量:
image = np.random.rand(224, 224, 3)
image = Image.fromarray((image * 255).astype(np.uint8))input_tensor = transform(image).unsqueeze(0) # 添加 batch 维度 [1, 3, 224, 224]# 4. 注册 Hook:获取第一个卷积层的输出
conv1_layer = model.features[0] # VGG16 第一个卷积层: Conv2d(3, 64, kernel_size=3, ...)
activation = {} # 存储特征图def get_activation(name):def hook(module, input, output):activation[name] = output.detach() # detach() 断开计算图return hook# 注册钩子
hook_handle = conv1_layer.register_forward_hook(get_activation('conv1'))# 5. 前向传播
with torch.no_grad():output = model(input_tensor)# 6. 移除 hook(可选,避免后续干扰)
hook_handle.remove()# 7. 获取特征图
conv1_features = activation['conv1'] # shape: [1, 64, H, W]
print(f"Feature map shape: {conv1_features.shape}") # e.g., [1, 64, 224, 224]# 8. 可视化前 16 个通道的特征图
def visualize_features(feature_maps, num=16, cmap='viridis'):feature_maps = feature_maps[0] # 去掉 batch 维度: [C, H, W]num = min(num, feature_maps.size(0)) # 最多显示 num 个rows = 4cols = 4fig, axes = plt.subplots(rows, cols, figsize=(12, 12))for i in range(num):r, c = i // cols, i % colsaxes[r, c].imshow(feature_maps[i].cpu().numpy(), cmap=cmap)axes[r, c].set_title(f'Filter {i+1}')axes[r, c].axis('off')plt.tight_layout()plt.show()visualize_features(conv1_features, num=16, cmap='gray')
2.3.CNN可视化方法--CAM
class activation map (CAM:类激活图)的作用是判断哪些变量对模型来说是重要的,在CNN可视化的场景下,即判断图像中哪些像素点对预测结果是重要的。除了确定重要的像素点,人们也会对重要区域的梯度感兴趣,因此在CAM的基础上也进一步改进得到了Grad-CAM(以及诸多变种)。CAM和Grad-CAM的示例如下图所示:
相比可视化卷积核与可视化特征图,CAM系列可视化更为直观,能够一目了然地确定重要区域,进而进行可解释性分析或模型优化改进。CAM系列操作的实现可以通过开源工具包pytorch-grad-cam来实现。
pip install grad-cam
import torch
from torchvision.models import vgg11
import matplotlib.pyplot as plt
from PIL import Image
import numpy as np# 设置中文字体支持
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False# 加载模型
model = vgg11(pretrained=True)
model.eval() # ⚠️ 记得设置为 eval 模式# 图像路径
img_path = './test.png'# 打开图像并转换为 RGB(关键!去除 alpha 通道)
img = Image.open(img_path).convert('RGB') # ← 强制转为 RGB
img = img.resize((224, 224))# 转为 numpy 数组 [0,1] 范围
rgb_img = np.float32(img) / 255# 显示原图
plt.imshow(img)
plt.title("Original Image")
plt.axis('off')
plt.show()
from pytorch_grad_cam import GradCAM
from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget
from pytorch_grad_cam.utils.image import show_cam_on_image# 转为 tensor
img_tensor = torch.from_numpy(rgb_img).permute(2, 0, 1).unsqueeze(0) # [1, 3, 224, 224]# 设置目标层(VGG11 最后一个卷积层)
target_layers = [model.features[-1]]# 初始化 Grad-CAM
cam = GradCAM(model=model, target_layers=target_layers)# 假设你想可视化类别预测结果中的某个类别
# 先运行前向传播得到预测结果
with torch.no_grad():output = model(img_tensor)
preds = output.argmax(dim=1).item() # 获取预测类别
print(f"Predicted class: {preds}")# 设置目标
targets = [ClassifierOutputTarget(preds)]# 生成 CAM
grayscale_cam = cam(input_tensor=img_tensor, targets=targets)
grayscale_cam = grayscale_cam[0] # 取第一个样本# 将 CAM 叠加到原图上
cam_image = show_cam_on_image(rgb_img, grayscale_cam, use_rgb=True)# 显示结果
plt.imshow(cam_image)
plt.title(f"Grad-CAM (Class: {preds})")
plt.axis('off')
plt.show()# 转为 PIL.Image 显示或保存
result_pil = Image.fromarray(cam_image)
result_pil.save("gradcam_result.jpg")
2.4.CNN可视化快速实现--FlashTorch
聪明的你可能要问了,已经202x年了,难道还要我们手把手去写各种CNN可视化的代码吗?答案当然是否定的。随着PyTorch社区的努力,目前已经有不少开源工具能够帮助我们快速实现CNN可视化。这里我们介绍其中的一个——FlashTorch。
(注:使用中发现该package对环境有要求,如果下方代码运行报错,请参考作者给出的配置或者Colab运行环境:https://github.com/MisaOgura/flashtorch/issues/39)
1.可视化梯度
import matplotlib.pyplot as plt
import torchvision.models as models
from flashtorch.utils import apply_transforms, load_image
from flashtorch.saliency import Backprop
import torch
import torch.nn as nn
import warnings
import flashtorch.utils as ft_utils
import flashtorch.saliency.backprop as ft_backprop#模型:AlexNet
model = models.alexnet(pretrained=True)# 将所有 ReLU 的 inplace 关闭,避免反向传播时对视图进行原地修改
model.eval()
for name, module in model.named_modules():if isinstance(module, nn.ReLU) and getattr(module, 'inplace', False):module.inplace = Falsebackprop = Backprop(model)#初始化Backpropimage = load_image('images/img.png')
owl = apply_transforms(image)#图像预处理# 运行时替换 flashtorch 的 denormalize,避免对视图进行原地修改
def _safe_denormalize(tensor, mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)):if tensor.requires_grad:tensor = tensor.detach()mean_tensor = torch.tensor(mean, dtype=tensor.dtype, device=tensor.device).view(-1, 1, 1)std_tensor = torch.tensor(std, dtype=tensor.dtype, device=tensor.device).view(-1, 1, 1)return tensor * std_tensor + mean_tensorft_utils.denormalize = _safe_denormalize
ft_backprop.denormalize = _safe_denormalizetarget_class = 24
use_gpu = torch.cuda.is_available()try:print('开始运行')backprop.visualize(owl, target_class, guided=True, use_gpu=use_gpu)print('全部完成')
except RuntimeError as e:if "is a view and is being modified inplace" in str(e):warnings.warn("Guided 反向传播在当前环境触发原地修改冲突,已自动回退到非 guided 模式。")backprop.visualize(owl, target_class, guided=False, use_gpu=use_gpu)else:raise
注意在jupyter运行
2.可视化卷积核
import torchvision.models as models
from flashtorch.activmax import GradientAscentmodel = models.vgg16(pretrained=True)
g_ascent = GradientAscent(model.features)# specify layer and filter info
conv5_1 = model.features[24]
conv5_1_filters = [45, 271, 363, 489]g_ascent.visualize(conv5_1, conv5_1_filters, title="VGG16: conv5_1")