Skip to content

Commit 77b19e2

Browse files
committed
feat(paper): add Grad-CAM
1 parent 97ce849 commit 77b19e2

3 files changed

Lines changed: 134 additions & 2 deletions

File tree

‎VERSION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.1.1
1+
1.1.2
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from typing import Optional
2+
import torch
3+
import torch.nn as nn
4+
import torch.nn.functional as F
5+
6+
7+
class GradCAM:
8+
def __init__(self, model: nn.Module, layer: int | nn.Module = -1):
9+
self.model = model
10+
11+
if isinstance(layer, int):
12+
self.layer = self.get_conv_layer(layer)
13+
else:
14+
self.layer = layer
15+
16+
self.A = None
17+
self.alpha = None
18+
19+
def get_conv_layer(self, n):
20+
"""
21+
retrieves the n-th convolutional layer from the model.
22+
23+
model: torch.nn.Module
24+
the neural network model.
25+
n: int
26+
the index of the convolutional layer to retrieve.
27+
"""
28+
conv_layers = []
29+
30+
# iterate through all modules and collect only convolutional layers
31+
for layer in self.model.modules():
32+
if isinstance(layer, nn.Conv2d):
33+
conv_layers.append(layer)
34+
35+
return conv_layers[n]
36+
37+
def forward_hook(self, module, input, output):
38+
"""
39+
hook to capture feature maps during the forward pass.
40+
41+
module: torch.nn.Module
42+
the convolutional layer being hooked.
43+
input: torch.Tensor
44+
the input tensor to the layer.
45+
output: torch.Tensor
46+
the output activations (feature maps).
47+
"""
48+
self.A = output
49+
50+
def backward_hook(self, module, grad_input, grad_output):
51+
"""
52+
hook to capture gradients during the backward pass.
53+
54+
module: torch.nn.Module
55+
the convolutional layer being hooked.
56+
grad_input: tuple
57+
gradients with respect to the input.
58+
grad_output: tuple
59+
gradients with respect to the output.
60+
"""
61+
self.alpha = grad_output[0].mean(dim=(-1, -2), keepdim=True)
62+
63+
def _grad_cam(
64+
self,
65+
img: torch.Tensor,
66+
class_idx: Optional[int | list | tuple | torch.Tensor] = None,
67+
):
68+
if img.dim() == 3:
69+
img = img.unsqueeze(0)
70+
71+
B, _, H, W = img.shape
72+
device = img.device
73+
74+
output = self.model(img)
75+
76+
if class_idx is None:
77+
target_classes = output.argmax(dim=1)
78+
elif isinstance(class_idx, int):
79+
target_classes = torch.tensor([class_idx] * B, device=device)
80+
elif isinstance(class_idx, (list, tuple)):
81+
if len(class_idx) != B:
82+
raise ValueError(
83+
f"Length of class_idx list ({len(class_idx)}) must match batch size ({B})"
84+
)
85+
target_classes = torch.tensor(class_idx, device=device)
86+
else:
87+
target_classes = class_idx
88+
89+
one_hot = torch.zeros_like(output)
90+
if isinstance(class_idx, int):
91+
one_hot[:, class_idx] = 1
92+
else:
93+
one_hot.scatter_(1, target_classes.view(-1, 1), 1)
94+
95+
self.model.zero_grad()
96+
output.backward(gradient=one_hot, retain_graph=False)
97+
98+
if self.A is None or self.alpha is None:
99+
raise RuntimeError("A or alpha is None.")
100+
101+
A = self.A.detach()
102+
alpha = self.alpha.detach()
103+
104+
gcam = torch.sum(A * alpha, dim=1, keepdim=True)
105+
gcam = F.relu(gcam)
106+
gcam = F.interpolate(gcam, (H, W), mode="bilinear", align_corners=False)
107+
108+
gcam = gcam.view(B, -1)
109+
gcam -= gcam.min(dim=1, keepdim=True)[0]
110+
gcam /= gcam.max(dim=1, keepdim=True)[0] + 1e-8
111+
gcam = gcam.view(B, 1, H, W)
112+
113+
return gcam
114+
115+
def __call__(
116+
self,
117+
img: torch.Tensor,
118+
class_idx: Optional[int | list | tuple | torch.Tensor] = None,
119+
):
120+
forward_handle = self.layer.register_forward_hook(self.forward_hook)
121+
backward_handle = self.layer.register_full_backward_hook(
122+
self.backward_hook
123+
)
124+
125+
try:
126+
self.model.eval()
127+
gcam = self._grad_cam(img, class_idx)
128+
finally:
129+
forward_handle.remove()
130+
backward_handle.remove()
131+
132+
return gcam

‎src/vicentin/papers/SimCLR/model.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import torch
33
import torch.nn as nn
44

5-
from vicentin.deep_learning.models.siamese import SiameseNetwork
5+
from vicentin.deep_learning.models import SiameseNetwork
66
from vicentin.deep_learning.blocks import MLP
77

88

0 commit comments

Comments
 (0)