Spaces:
Sleeping
Sleeping
Download gradcam.py from Shuxian/SGDNet-Fish-Classification: direct link, hf CLI and curl.
- Browser
- Download file 6.37 kB
-
https://huggingface.co/spaces/Shuxian/SGDNet-Fish-Classification/resolve/main/gradcam.py
- Command line
-
hf download hf://spaces/Shuxian/SGDNet-Fish-Classification/gradcam.py
-
curl -L -o gradcam.py https://huggingface.co/spaces/Shuxian/SGDNet-Fish-Classification/resolve/main/gradcam.py
6.37 kB
| """ | |
| Grad-CAM implementation for visualization | |
| """ | |
| import torch | |
| import torch.nn.functional as F | |
| import numpy as np | |
| class GradCAM: | |
| """Grad-CAM实现""" | |
| def __init__(self, model, target_layer): | |
| self.model = model | |
| self.target_layer = target_layer | |
| self.gradients = None | |
| self.activations = None | |
| self.forward_handle = None | |
| self.backward_handle = None | |
| # 强制开启梯度(确保梯度能回传) | |
| for param in self.model.parameters(): | |
| param.requires_grad = True | |
| # 注册hook | |
| self.hook_layers() | |
| def hook_layers(self): | |
| """注册forward和backward hook""" | |
| if self.forward_handle is not None: | |
| self.forward_handle.remove() | |
| if self.backward_handle is not None: | |
| self.backward_handle.remove() | |
| def backward_hook(module, grad_in, grad_out): | |
| """标准的backward hook签名:(module, grad_in, grad_out)""" | |
| if grad_out[0] is not None: | |
| self.gradients = grad_out[0] | |
| def forward_hook(module, input, output): | |
| """forward hook保存激活值""" | |
| self.activations = output | |
| self.forward_handle = self.target_layer.register_forward_hook(forward_hook) | |
| self.backward_handle = self.target_layer.register_backward_hook(backward_hook) | |
| def generate_cam(self, input_tensor, class_idx=None): | |
| """生成CAM热力图""" | |
| self.gradients = None | |
| self.activations = None | |
| # 设置为train模式以允许梯度回传 | |
| self.model.train() | |
| # 禁用dropout和batchnorm的随机性 | |
| for m in self.model.modules(): | |
| if isinstance(m, (torch.nn.BatchNorm2d, torch.nn.Dropout)): | |
| m.eval() | |
| input_tensor = input_tensor.requires_grad_(True) | |
| # Forward pass | |
| output = self.model(input_tensor) | |
| # 如果是SGD-Net,取分类输出 | |
| if isinstance(output, tuple): | |
| output = output[0] | |
| # 如果没有指定类别,使用预测类别 | |
| if class_idx is None: | |
| class_idx = output.argmax(dim=1).item() | |
| # Backward pass | |
| self.model.zero_grad() | |
| output[0, class_idx].backward(retain_graph=True) | |
| # 如果gradients仍然是None或全0,使用fallback方法 | |
| if self.gradients is None or (hasattr(self.gradients, 'abs') and self.gradients.abs().max().item() < 1e-8): | |
| if self.activations is not None: | |
| if len(self.activations.shape) == 4: | |
| activations = self.activations[0] # (C, H, W) | |
| else: | |
| activations = self.activations | |
| cam = torch.mean(activations.abs(), dim=0) # (H, W) | |
| cam_min = cam.min() | |
| cam_max = cam.max() | |
| if cam_max > cam_min + 1e-8: | |
| cam = (cam - cam_min) / (cam_max - cam_min) | |
| else: | |
| cam = torch.ones_like(cam) * 0.5 | |
| return cam.detach().cpu().numpy() | |
| if self.gradients is None or self.activations is None: | |
| return np.ones((8, 8)) * 0.5 | |
| # 计算权重(全局平均池化梯度) | |
| if len(self.gradients.shape) == 4: | |
| gradients = self.gradients[0] # (C, H, W) | |
| else: | |
| gradients = self.gradients | |
| if len(self.activations.shape) == 4: | |
| activations = self.activations[0] # (C, H, W) | |
| else: | |
| activations = self.activations | |
| # 计算权重(对每个通道的梯度进行全局平均池化) | |
| weights = torch.mean(gradients, dim=(1, 2), keepdim=True) # (C, 1, 1) | |
| # 加权求和 | |
| cam = torch.sum(weights * activations, dim=0) # (H, W) | |
| cam = F.relu(cam) # ReLU(只保留正梯度) | |
| # 移除左上角异常高值(针对SGD-Net) | |
| h, w = cam.shape | |
| corner_h = h // 3 | |
| corner_w = w // 3 | |
| corner_region = cam[:corner_h, :corner_w] | |
| rest_region = torch.cat([ | |
| cam[corner_h:, :].flatten(), | |
| cam[:, corner_w:].flatten() | |
| ]) | |
| if rest_region.numel() > 0: | |
| rest_median = torch.median(rest_region) | |
| rest_std = torch.std(rest_region) | |
| corner_max = torch.max(corner_region) | |
| corner_mean = torch.mean(corner_region) | |
| if corner_max > rest_median + 1.5 * rest_std or corner_mean > rest_median + 0.5 * rest_std: | |
| for i in range(corner_h): | |
| for j in range(corner_w): | |
| dist_to_edge = min(i, j) | |
| weight = max(0.0, 1.0 - dist_to_edge / max(corner_h, corner_w) * 2.0) | |
| weight = min(1.0, weight) | |
| cam[i, j] = cam[i, j] * (1 - weight) + rest_median * weight | |
| # 归一化 | |
| cam_min = cam.min() | |
| cam_max = cam.max() | |
| if cam_max > cam_min + 1e-8: | |
| cam = (cam - cam_min) / (cam_max - cam_min) | |
| else: | |
| cam = torch.ones_like(cam) * 0.5 | |
| cam = torch.clamp(cam, 0, 1) | |
| return cam.detach().cpu().numpy() | |
| def __del__(self): | |
| """清理hook""" | |
| if self.forward_handle is not None: | |
| self.forward_handle.remove() | |
| if self.backward_handle is not None: | |
| self.backward_handle.remove() | |
| def get_target_layer(model): | |
| """获取目标层用于Grad-CAM(针对ResNetSGDNet)""" | |
| backbone = model.backbone | |
| # ResNet - 取layer4的最后一个block的最后一个conv | |
| if hasattr(backbone, 'layer4'): | |
| layer4 = backbone.layer4 | |
| if hasattr(layer4, '__len__') and len(layer4) > 0: | |
| last_block = layer4[-1] | |
| if hasattr(last_block, 'conv3'): | |
| return last_block.conv3 | |
| elif hasattr(last_block, 'conv2'): | |
| return last_block.conv2 | |
| else: | |
| return last_block | |
| else: | |
| return layer4 | |
| else: | |
| # 默认取最后一个模块 | |
| modules = list(backbone.children()) | |
| return modules[-1] if modules else None | |