news 2026/3/25 16:38:02

Day45 Grad-CAM 与 Hook 函数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Day45 Grad-CAM 与 Hook 函数

@浙大疏锦行

知识点回顾

1.回调函数

# 定义一个回调函数 def handle_result(result): """处理计算结果的回调函数""" print(f"计算结果是: {result}") # 定义一个接受回调函数的函数 def calculate(a, b, callback): # callback是一个约定俗成的参数名 """ 这个函数接受两个数值和一个回调函数,用于处理计算结果。 执行计算并调用回调函数 """ result = a + b callback(result) # 在计算完成后调用回调函数 # 使用回调函数 calculate(3, 5, handle_result) # 输出: 计算结果是: 8

2.lambda 函数

# 定义匿名函数:计算平方 square = lambda x: x ** 2 # 调用 print(square(5)) # 输出: 25

3.hook 函数的模块钩子和张量钩子

import torch import torch.nn as nn import numpy as np import matplotlib.pyplot as plt # 设置随机种子,保证结果可复现 torch.manual_seed(42) np.random.seed(42) import torch import torch.nn as nn # 定义一个简单的卷积神经网络模型 class SimpleModel(nn.Module): def __init__(self): super(SimpleModel, self).__init__() # 定义卷积层:输入通道1,输出通道2,卷积核3x3,填充1保持尺寸不变 self.conv = nn.Conv2d(1, 2, kernel_size=3, padding=1) # 定义ReLU激活函数 self.relu = nn.ReLU() # 定义全连接层:输入特征2*4*4,输出10分类 self.fc = nn.Linear(2 * 4 * 4, 10) def forward(self, x): # 卷积操作 x = self.conv(x) # 激活函数 x = self.relu(x) # 展平为一维向量,准备输入全连接层 x = x.view(-1, 2 * 4 * 4) # 全连接分类 x = self.fc(x) return x # 创建模型实例 model = SimpleModel() # 创建一个列表用于存储中间层的输出 conv_outputs = [] # 定义前向钩子函数 - 用于在模型前向传播过程中获取中间层信息 def forward_hook(module, input, output): """ 前向钩子函数,会在模块每次执行前向传播后被自动调用 参数: module: 当前应用钩子的模块实例 input: 传递给该模块的输入张量元组 output: 该模块产生的输出张量 """ print(f"钩子被调用!模块类型: {type(module)}") print(f"输入形状: {input[0].shape}") # input是一个元组,对应 (image, label) print(f"输出形状: {output.shape}") # 保存卷积层的输出用于后续分析 # 使用detach()避免追踪梯度,防止内存泄漏 conv_outputs.append(output.detach()) # 在卷积层注册前向钩子 # register_forward_hook返回一个句柄,用于后续移除钩子 hook_handle = model.conv.register_forward_hook(forward_hook) # 创建一个随机输入张量 (批次大小=1, 通道=1, 高度=4, 宽度=4) x = torch.randn(1, 1, 4, 4) # 执行前向传播 - 此时会自动触发钩子函数 output = model(x) # 释放钩子 - 重要!防止在后续模型使用中持续调用钩子造成意外行为或内存泄漏 hook_handle.remove() # # 打印中间层输出结果 # if conv_outputs: # print(f"\n卷积层输出形状: {conv_outputs[0].shape}") # print(f"卷积层输出值示例: {conv_outputs[0][0, 0, :, :]}") # 定义一个存储梯度的列表 conv_gradients = [] # 定义反向钩子函数 def backward_hook(module, grad_input, grad_output): # 模块:当前应用钩子的模块 # grad_input:模块输入的梯度 # grad_output:模块输出的梯度 print(f"反向钩子被调用!模块类型: {type(module)}") print(f"输入梯度数量: {len(grad_input)}") print(f"输出梯度数量: {len(grad_output)}") # 保存梯度供后续分析 conv_gradients.append((grad_input, grad_output)) # 在卷积层注册反向钩子 hook_handle = model.conv.register_backward_hook(backward_hook) # 创建一个随机输入并进行前向传播 x = torch.randn(1, 1, 4, 4, requires_grad=True) output = model(x) # 定义一个简单的损失函数并进行反向传播 loss = output.sum() loss.backward() # 释放钩子 hook_handle.remove()

4.Grad-CAM 的示例

import torch import torch.nn as nn import torch.nn.functional as F import torchvision import torchvision.transforms as transforms import numpy as np import matplotlib.pyplot as plt from PIL import Image # 设置随机种子确保结果可复现 # 在深度学习中,随机种子可以让每次运行代码时,模型初始化参数、数据打乱等随机操作保持一致,方便调试和对比实验结果 torch.manual_seed(42) np.random.seed(42) # 加载CIFAR-10数据集 # 定义数据预处理步骤,先将图像转换为张量,再进行归一化操作 # 归一化的均值和标准差是(0.5, 0.5, 0.5),这里的均值和标准差是对CIFAR-10数据集的经验值,使得数据分布更有利于模型训练 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) # 加载测试集,指定数据集根目录为'./data',设置为测试集(train=False),如果数据不存在则下载(download=True),并应用上述定义的预处理 testset = torchvision.datasets.CIFAR10( root='./data', train=False, download=True, transform=transform ) # 定义类别名称,CIFAR-10数据集包含这10个类别 classes = ('飞机', '汽车', '鸟', '猫', '鹿', '狗', '青蛙', '马', '船', '卡车') # 定义一个简单的CNN模型 class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() # 第一个卷积层,输入通道为3(彩色图像),输出通道为32,卷积核大小为3x3,填充为1以保持图像尺寸不变 self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1) # 第二个卷积层,输入通道为32,输出通道为64,卷积核大小为3x3,填充为1 self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) # 第三个卷积层,输入通道为64,输出通道为128,卷积核大小为3x3,填充为1 self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1) # 最大池化层,池化核大小为2x2,步长为2,用于下采样,减少数据量并提取主要特征 self.pool = nn.MaxPool2d(2, 2) # 第一个全连接层,输入特征数为128 * 4 * 4(经过前面卷积和池化后的特征维度),输出为512 self.fc1 = nn.Linear(128 * 4 * 4, 512) # 第二个全连接层,输入为512,输出为10(对应CIFAR-10的10个类别) self.fc2 = nn.Linear(512, 10) def forward(self, x): # 第一个卷积层后接ReLU激活函数和最大池化操作,经过池化后图像尺寸变为原来的一半,这里输出尺寸变为16x16 x = self.pool(F.relu(self.conv1(x))) # 第二个卷积层后接ReLU激活函数和最大池化操作,输出尺寸变为8x8 x = self.pool(F.relu(self.conv2(x))) # 第三个卷积层后接ReLU激活函数和最大池化操作,输出尺寸变为4x4 x = self.pool(F.relu(self.conv3(x))) # 将特征图展平为一维向量,以便输入到全连接层 x = x.view(-1, 128 * 4 * 4) # 第一个全连接层后接ReLU激活函数 x = F.relu(self.fc1(x)) # 第二个全连接层输出分类结果 x = self.fc2(x) return x # 初始化模型 model = SimpleCNN() print("模型已创建") # 如果有GPU则使用GPU,将模型转移到对应的设备上 device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") model = model.to(device) # 训练模型(简化版,实际应用中应该进行完整训练) def train_model(model, epochs=1): # 加载训练集,指定数据集根目录为'./data',设置为训练集(train=True),如果数据不存在则下载(download=True),并应用前面定义的预处理 trainset = torchvision.datasets.CIFAR10( root='./data', train=True, download=True, transform=transform ) # 创建数据加载器,设置批量大小为64,打乱数据顺序(shuffle=True),使用2个线程加载数据 trainloader = torch.utils.data.DataLoader( trainset, batch_size=64, shuffle=True, num_workers=2 ) # 定义损失函数为交叉熵损失,用于分类任务 criterion = nn.CrossEntropyLoss() # 定义优化器为Adam,用于更新模型参数,学习率设置为0.001 optimizer = torch.optim.Adam(model.parameters(), lr=0.001) for epoch in range(epochs): running_loss = 0.0 for i, data in enumerate(trainloader, 0): # 从数据加载器中获取图像和标签 inputs, labels = data # 将图像和标签转移到对应的设备(GPU或CPU)上 inputs, labels = inputs.to(device), labels.to(device) # 清空梯度,避免梯度累加 optimizer.zero_grad() # 模型前向传播得到输出 outputs = model(inputs) # 计算损失 loss = criterion(outputs, labels) # 反向传播计算梯度 loss.backward() # 更新模型参数 optimizer.step() running_loss += loss.item() if i % 100 == 99: # 每100个批次打印一次平均损失 print(f'[{epoch + 1}, {i + 1}] 损失: {running_loss / 100:.3f}') running_loss = 0.0 print("训练完成") # 训练模型(可选,如果有预训练模型可以加载) # 取消下面这行的注释来训练模型 # train_model(model, epochs=1) # 或者尝试加载预训练模型(如果存在) try: # 尝试加载名为'cifar10_cnn.pth'的模型参数 model.load_state_dict(torch.load('cifar10_cnn.pth')) print("已加载预训练模型") except: print("无法加载预训练模型,使用未训练模型或训练新模型") # 如果没有预训练模型,可以在这里调用train_model函数 train_model(model, epochs=1) # 保存训练后的模型参数 torch.save(model.state_dict(), 'cifar10_cnn.pth') # 设置模型为评估模式,此时模型中的一些操作(如dropout、batchnorm等)会切换到评估状态 model.eval() # Grad-CAM实现 class GradCAM: def __init__(self, model, target_layer): self.model = model self.target_layer = target_layer self.gradients = None self.activations = None # 注册钩子,用于获取目标层的前向传播输出和反向传播梯度 self.register_hooks() def register_hooks(self): # 前向钩子函数,在目标层前向传播后被调用,保存目标层的输出(激活值) def forward_hook(module, input, output): self.activations = output.detach() # 反向钩子函数,在目标层反向传播后被调用,保存目标层的梯度 def backward_hook(module, grad_input, grad_output): self.gradients = grad_output[0].detach() # 在目标层注册前向钩子和反向钩子 self.target_layer.register_forward_hook(forward_hook) self.target_layer.register_backward_hook(backward_hook) def generate_cam(self, input_image, target_class=None): # 前向传播,得到模型输出 model_output = self.model(input_image) if target_class is None: # 如果未指定目标类别,则取模型预测概率最大的类别作为目标类别 target_class = torch.argmax(model_output, dim=1).item() # 清除模型梯度,避免之前的梯度影响 self.model.zero_grad() # 反向传播,构造one-hot向量,使得目标类别对应的梯度为1,其余为0,然后进行反向传播计算梯度 one_hot = torch.zeros_like(model_output) one_hot[0, target_class] = 1 model_output.backward(gradient=one_hot) # 获取之前保存的目标层的梯度和激活值 gradients = self.gradients activations = self.activations # 对梯度进行全局平均池化,得到每个通道的权重,用于衡量每个通道的重要性 weights = torch.mean(gradients, dim=(2, 3), keepdim=True) # 加权激活映射,将权重与激活值相乘并求和,得到类激活映射的初步结果 cam = torch.sum(weights * activations, dim=1, keepdim=True) # ReLU激活,只保留对目标类别有正贡献的区域,去除负贡献的影响 cam = F.relu(cam) # 调整大小并归一化,将类激活映射调整为与输入图像相同的尺寸(32x32),并归一化到[0, 1]范围 cam = F.interpolate(cam, size=(32, 32), mode='bilinear', align_corners=False) cam = cam - cam.min() cam = cam / cam.max() if cam.max() > 0 else cam return cam.cpu().squeeze().numpy(), target_class
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/15 9:55:06

Jupyter调试模型技巧,开发者必备技能

Jupyter调试模型技巧,开发者必备技能 在实际使用 Z-Image-ComfyUI 进行图像生成开发时,很多开发者会卡在一个看似简单却影响深远的环节:模型跑通了,但效果不理想;工作流能加载,但改了参数没反应&#xff1…

作者头像 李华
网站建设 2026/3/24 15:23:44

StructBERT实战教程:从源码结构理解Siamese双分支特征提取

StructBERT实战教程:从源码结构理解Siamese双分支特征提取 1. 为什么需要专门的中文语义匹配工具? 你有没有遇到过这样的问题:用通用文本编码模型计算两段完全无关的中文内容相似度,结果却显示0.65?比如“苹果手机发…

作者头像 李华
网站建设 2026/3/25 7:35:51

Swin2SR应用场景:电商商品图模糊放大解决方案

Swin2SR应用场景:电商商品图模糊放大解决方案 1. 为什么电商商家总在为商品图发愁? 你有没有遇到过这些情况? 刚收到供应商发来的商品图,只有640480,放大后全是马赛克,连标签上的字都看不清; …

作者头像 李华
网站建设 2026/3/24 22:19:39

无需PS也能做证件照?AI工坊全自动流程部署实战案例

无需PS也能做证件照?AI工坊全自动流程部署实战案例 1. 这不是PS插件,而是一套能“自己干活”的证件照生产线 你有没有过这样的经历:临时要交简历照片,翻遍手机相册却找不到一张合适的正面照;赶着办护照,发…

作者头像 李华
网站建设 2026/3/25 7:08:41

Z-Image Turbo简化流程:告别手动调试依赖版本问题

Z-Image Turbo简化流程:告别手动调试依赖版本问题 1. 为什么你总在“装不起来”和“一跑就黑”之间反复横跳? 你是不是也经历过这些时刻: 下载好Z-Image-Turbo模型,兴冲冲打开启动脚本,结果卡在ImportError: cannot…

作者头像 李华