一、直方图可视化数据分布
1. 知识介绍
在PyTorch 模型的每一层注册一个 forward hook,从而能够捕获每层的输出
简单列表存储形式(只能顺序查看每层输出,下文会有改进版用字典将层名字和层输出值对应)
activations = []
def hook_fn(module, input, output):
activations.append(output.detach()) # 记录输出并去除计算图
# 注册 hook
for layer in model.children():
layer.register_forward_hook(hook_fn)
#列表存储版本 可视化
for i, activation in enumerate(activations):
plt.figure(figsize=(6, 4))
plt.hist(activation.numpy().flatten(), bins=100)
plt.title(f'Layer {i + 1} Activation Distribution')
plt.xlabel('Activation Value')
plt.ylabel('Frequency')
plt.show()
1. 什么是 Hook?
在 PyTorch 中,hook 是一种机制,允许你在模型的前向传播(
forward
)或反向传播(backward
)过程中插入自定义操作。具体来说,forward hook 允许你在每一层的输出被计算之后,执行一些额外的操作,比如记录激活值、修改输出等。2.model.children() 的作用
model.children() 是一个 PyTorch 中的迭代器,用来返回模型中的每个子模块(子层)。对于一个 nn.Module(即神经网络模型),它的子模块可以是层(例如 nn.Linear,nn.Conv2d)或者其他子网络(例如子模型)。model.children()返回的是这些子模块的迭代器,它可以让你遍历模型中的每一层。
3. register_forward_hook的作用
register_forward_hook是一个方法,它可以在你对模型进行前向传播时,将一个 hook 函数(
hook_fn
)注册到每一层。这个函数会在每次经过该层时被调用,从而允许你对输出数据进行进一步的处理、修改或记录。
2. 代码实战
2.1 关键步骤
1. 捕获激活值:
- 使用 forward_hook 在前向传播时捕获每一层的激活值,并存储在字典 activations 中,键为层的名字,值为层的输出。
def hook_fn(module, input, output):
for name, layer in model.named_children():
if layer == module: # 如果当前层匹配
activations[name] = output.detach() # 将输出存储到字典中
- 注册 forward_hook:使用 register_forward_hook 方法
for layer in model.children():
layer.register_forward_hook(hook_fn)
解释:hook_fn 每层的前向传播时被调用,它确保捕获全连接层(fc1, fc2, fc3)的激活值。
2. 激活值分布的直方图可视化(单独显示每层的分布):
-
遍历每一层的激活值,并将其转化为一维数组,通过 plt.hist() 生成直方图,展示激活值的分布:
def plot_activation_distributions(activations):
for i, (layer, activation) in enumerate(activations.items()):
plt.figure(figsize=(10, 6))
plt.hist(activation.cpu().numpy().flatten(), bins=50, alpha=0.7, color='blue')
plt.title(f'Activation distribution at layer {layer}')
plt.xlabel('Activation value')
plt.ylabel('Frequency')
plt.grid(True)
plt.show()
解释:
- activation.cpu().numpy().flatten() 将多维张量拉平为一维数组,便于绘制直方图。
- 通过直方图,观察模型不同层的激活值是否存在较多的饱和情况(例如接近 0 或 1),帮助诊断网络是否正常工作。
3. 激活值分布的子图可视化(同时显示多个层的分布):
-
使用 plt.subplots 创建多个子图,将每一层的激活分布绘制在同一张图中:
fig, axes = plt.subplots(1, len(activations), figsize=(15, 5))
for i, (name, activation) in enumerate(activations.items()):
axes[i].hist(activation.numpy().flatten(), bins=50, alpha=0.75)
axes[i].set_title(f'Activation Distribution ({name})')
axes[i].set_xlabel('Activation Value')
axes[i].set_ylabel('Frequency')
plt.tight_layout()
plt.show()
解释:
- 每个子图显示一层的激活分布,通过对比不同层的激活值分布,可以分析网络在不同深度的特征提取效果。
- plt.subplots 按照层的数量自动生成相应数量的子图,整齐排列。
2.2 完整代码
import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
class SimpleMLP(nn.Module):
def __init__(self):
super(SimpleMLP, self).__init__()
self.fc1 = nn.Linear(784, 256)
self.fc2 = nn.Linear(256, 128)
self.fc3 = nn.Linear(128, 10)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
x = self.fc3(x)
return x
model = SimpleMLP()
input = torch.randn(1, 784)
#用字典形式存储
activations = {}
def hook_fn(module, input, output):
for name, layer in model.named_children():
if layer == module: # 如果层对象和当前的模块对象匹配
activations[name] = output.detach() # 使用层的名字作为键,存储输出
for layer in model.children():
layer.register_forward_hook(hook_fn)
output = model(input)
for layer_name, activation in activations.items():
print(f"{layer_name} activation shape: {activation.shape}")
# 1.一张张显示
def plot_activation_distributions(activations):
for i, (layer, activation) in enumerate(activations.items()):
plt.figure(figsize=(10, 6))
plt.hist(activation.cpu().numpy().flatten(), bins=50, alpha=0.7, color='blue')
plt.title(f'Activation distribution at layer {layer}')
plt.xlabel('Activation value')
plt.ylabel('Frequency')
plt.grid(True)
plt.show()
# 绘制每一层的激活分布
plot_activation_distributions(activations)
# 2.排列好,子图显示
fig, axes = plt.subplots(1, len(activations), figsize=(15, 5))
# 如果只有一个子图,axes 不是数组,要进行特殊处理
if len(activations) == 1:
axes = [axes]
# 绘制每一层的激活分布
for i, (name, activation) in enumerate(activations.items()):
axes[i].hist(activation.numpy().flatten(), bins=50, alpha=0.75)
axes[i].set_title(f'Activation Distribution ({name})')
axes[i].set_xlabel('Activation Value')
axes[i].set_ylabel('Frequency')
plt.tight_layout()
plt.show()
代码效果:
二、热力图可视化关注重点
1.知识介绍
热力图确实是针对卷积层的,因为卷积层的输出通常是多个通道的二维特征图,而普通全连接层(如MLP)的输出是一个一维向量,无法直接生成热力图。
这种方法主要用于卷积神经网络(CNN),帮助我们理解模型在处理图像时关注的特征区域,特别是在分类任务中。通过热力图,我们可以可视化网络在进行预测时,关注的是输入图像的哪些部分。常用的可视化方法有 Grad-CAM 和 Guided Grad-CAM。
Grad-CAM
Grad-CAM(Gradient-weighted Class Activation Mapping)是一个基于梯度的热力图生成方法,它利用最后一层卷积层的梯度信息,生成一个反映网络关注区域的热力图。
2. 代码实战
2.1 关键步骤
(1)捕获激活值和梯度:
- 激活值(activations):记录卷积层的输出。
- 梯度值(gradients):记录目标类别在反向传播时对卷积层输出的梯度。
def forward_hook(module, input, output):
activations[module] = output
def backward_hook(module, grad_input, grad_output):
gradients[module] = grad_output[0]
2. 计算梯度权重:
- 对每个通道的梯度取均值,生成通道权重,表示每个特征图对目标类别的贡献:
weights = torch.mean(gradient, dim=(1, 2))
3. 生成 Grad-CAM 热力图:
- 用通道权重加权激活值,得到二维热力图
cam = torch.zeros(activation.shape[1:], dtype=torch.float32)
for i, w in enumerate(weights):
cam += w * activation[i]
cam = F.relu(cam)
cam = (cam - cam.min()) / (cam.max() - cam.min())
4. 叠加热力图到原图:
- 使用 cv2.addWeighted 将 Grad-CAM 热力图叠加到原始图片上,实现最终可视化效果:
overlayed_image = cv2.addWeighted(original_image, 1 - alpha, heatmap, alpha, 0)
2.2 完整代码
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision import transforms
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np
import cv2
# 定义一个简单的 CNN 模型
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.fc1 = nn.Linear(7*7*64, 1024)
self.fc2 = nn.Linear(1024, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool(x)
x = F.relu(self.conv2(x))
x = self.pool(x)
x = x.view(x.size(0), -1)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
# 实例化模型
model = SimpleCNN()
# 注册 Hook 捕获激活值和梯度
activations = {}
gradients = {}
def forward_hook(module, input, output):
activations[module] = output
def backward_hook(module, grad_input, grad_output):
gradients[module] = grad_output[0]
# 注册 Hook 在目标层
target_layer = model.conv2
target_layer.register_forward_hook(forward_hook)
target_layer.register_backward_hook(backward_hook)
# 加载图片并预处理
def preprocess_image(image_path):
img = Image.open(image_path).convert('L') # 转为灰度图
transform = transforms.Compose([
transforms.Resize((28, 28)), # 调整大小为 28x28
transforms.ToTensor(), # 转为 Tensor 并归一化到 [0, 1]
])
img_tensor = transform(img).unsqueeze(0) # 增加批次维度
return img_tensor, img
# 生成 Grad-CAM 热力图
def generate_gradcam(model, input_image, target_class):
model.eval()
# 前向传播
output = model(input_image)
# 计算目标类别的梯度
model.zero_grad()
class_score = output[0, target_class] # 选择目标类别
class_score.backward()
# 获取目标层的激活值和梯度
activation = activations[target_layer].squeeze(0) # Shape: (C, H, W)
gradient = gradients[target_layer].squeeze(0) # Shape: (C, H, W)
# 计算 Grad-CAM 权重
weights = torch.mean(gradient, dim=(1, 2)) # 对每个通道的梯度取均值
cam = torch.zeros(activation.shape[1:], dtype=torch.float32) # Shape: (H, W)
# 加权求和
for i, w in enumerate(weights):
cam += w * activation[i]
# 通过 ReLU 去掉负值
cam = F.relu(cam)
# 归一化到 [0, 1]
cam = (cam - cam.min()) / (cam.max() - cam.min())
return cam
# 将热力图叠加到原始图片上
def overlay_heatmap_on_image(cam, original_image, alpha=0.5, cmap='jet'):
cam = cam.detach().cpu().numpy() # 转为 numpy
cam = Image.fromarray(np.uint8(255 * cam)).resize(original_image.size, Image.BICUBIC) # 调整到原图大小
cam = np.array(cam)
# 将热力图转换为彩色
heatmap = plt.get_cmap(cmap)(cam / 255.0)[:, :, :3] # (H, W, 3)
heatmap = np.uint8(255 * heatmap)
# 叠加热力图到原图
original_image = np.array(original_image.convert("RGB"))
overlayed_image = cv2.addWeighted(original_image, 1 - alpha, heatmap, alpha, 0)
return overlayed_image
# 图片路径
image_path = r'D:\code_python\000adeep_learning\test\1.jpg' # 替换为你的图片路径
# 读入并预处理图片
input_image, original_image = preprocess_image(image_path)
# 生成 Grad-CAM 热力图
target_class = 0 # 假设我们关注类别 0
cam = generate_gradcam(model, input_image, target_class)
# 将热力图叠加到原始图片
overlayed_image = overlay_heatmap_on_image(cam, original_image, alpha=0.5)
# 显示叠加图像
plt.imshow(overlayed_image)
plt.axis('off')
plt.show()
代码效果:
标签:plt,训练,image,hook,神经网络,可视化,cam,activations,self From: https://blog.csdn.net/m0_73660403/article/details/144740849