DoB24 commited on
Commit
02ec143
·
verified ·
1 Parent(s): 531272b

gradcam v2: generator code

Browse files
Files changed (1) hide show
  1. code/generate_gradcam_v2.py +238 -0
code/generate_gradcam_v2.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generate GradCAM / attention saliency for all 9 v2 models.
2
+
3
+ Loads fresh v2 weights from weights_v2/ and weights_v3/ and writes per-class
4
+ grid PNGs into gradcam_v2/. CNN-class models use GradCAM on the final conv
5
+ block. CLIP uses input-gradient saliency. Vision-Transformer foundation
6
+ models (Swin-B, DINOv2-L, RETFound) use pytorch-grad-cam with the appropriate
7
+ reshape_transform.
8
+ """
9
+ from __future__ import annotations
10
+ import json, sys
11
+ from pathlib import Path
12
+ import numpy as np
13
+ import torch
14
+ import torch.nn as nn
15
+ import matplotlib
16
+ matplotlib.use("Agg")
17
+ import matplotlib.pyplot as plt
18
+ from PIL import Image
19
+ from torchvision import models, transforms
20
+
21
+ from pytorch_grad_cam import GradCAM
22
+ from pytorch_grad_cam.utils.image import show_cam_on_image
23
+
24
+ ROOT = Path("/home/bytical/fundus_project")
25
+ sys.path.insert(0, str(ROOT / "comparison_experiment"))
26
+
27
+ MANIFEST = ROOT / "holdout_split_augmented.json"
28
+ OUT_DIR = ROOT / "gradcam_v2"
29
+ OUT_DIR.mkdir(exist_ok=True)
30
+ W_V2 = ROOT / "weights_v2"
31
+ W_V3 = ROOT / "weights_v3"
32
+
33
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
34
+ IMAGENET_MEAN = [0.485, 0.456, 0.406]
35
+ IMAGENET_STD = [0.229, 0.224, 0.225]
36
+ CLASS_FULL = ["CSC", "DR", "Disc Edema", "Glaucoma", "Healthy",
37
+ "Macular Scar", "Myopia", "Pterygium", "Retinal Det.", "Retinitis Pig."]
38
+
39
+ def tf_for(size):
40
+ return transforms.Compose([
41
+ transforms.Resize((size, size)),
42
+ transforms.ToTensor(),
43
+ transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),
44
+ ])
45
+
46
+ # ---------- CNN builders matching v2 training ----------
47
+ def build_cnn(name, num_classes=10):
48
+ if name == "vgg19":
49
+ m = models.vgg19(weights=None)
50
+ m.classifier[-1] = nn.Linear(m.classifier[-1].in_features, num_classes)
51
+ return m, m.features[-1], 224
52
+ if name == "resnet50":
53
+ m = models.resnet50(weights=None); m.fc = nn.Linear(m.fc.in_features, num_classes)
54
+ return m, m.layer4[-1], 224
55
+ if name == "resnet101":
56
+ m = models.resnet101(weights=None); m.fc = nn.Linear(m.fc.in_features, num_classes)
57
+ return m, m.layer4[-1], 224
58
+ if name == "densenet121":
59
+ m = models.densenet121(weights=None)
60
+ m.classifier = nn.Linear(m.classifier.in_features, num_classes)
61
+ return m, m.features.norm5, 224
62
+ if name == "inception_v3":
63
+ m = models.inception_v3(weights=None, aux_logits=True)
64
+ m.fc = nn.Linear(m.fc.in_features, num_classes)
65
+ m.AuxLogits.fc = nn.Linear(m.AuxLogits.fc.in_features, num_classes)
66
+ return m, m.Mixed_7c, 299
67
+ raise ValueError(name)
68
+
69
+ def build_clip(num_classes=10):
70
+ import open_clip
71
+ base, _, _ = open_clip.create_model_and_transforms("ViT-B-16", pretrained="openai")
72
+ class Wrap(nn.Module):
73
+ def __init__(self):
74
+ super().__init__()
75
+ self.backbone = base.visual # match run_v2_experiments.CLIPClf
76
+ d = self.backbone.output_dim if hasattr(self.backbone, "output_dim") else 512
77
+ self.head = nn.Linear(d, num_classes)
78
+ def forward(self, x):
79
+ return self.head(self.backbone(x).float())
80
+ return Wrap(), 224
81
+
82
+ # ---------- Foundation builders (mirror run_foundation_models.py) ----------
83
+ def build_swin(num_classes=10):
84
+ import timm
85
+ m = timm.create_model("swin_base_patch4_window7_224", pretrained=False, num_classes=num_classes)
86
+ return m, 224
87
+
88
+ def build_dinov2(num_classes=10):
89
+ import timm
90
+ backbone = torch.hub.load("facebookresearch/dinov2", "dinov2_vitl14", source="github")
91
+ class Wrap(nn.Module):
92
+ def __init__(self):
93
+ super().__init__()
94
+ self.backbone = backbone
95
+ self.head = nn.Linear(1024, num_classes)
96
+ def forward(self, x):
97
+ f = self.backbone(x)
98
+ return self.head(f)
99
+ return Wrap(), 224
100
+
101
+ def build_retfound(num_classes=10):
102
+ import timm
103
+ m = timm.create_model("vit_large_patch16_224", pretrained=False, num_classes=num_classes,
104
+ global_pool="avg")
105
+ return m, 224
106
+
107
+ # ---------- Reshape transforms for ViT-style ----------
108
+ def vit_reshape(t, h=14, w=14):
109
+ # t: [B, tokens, dim] -> [B, dim, h, w]; skip CLS if present
110
+ if t.shape[1] == h*w + 1:
111
+ t = t[:, 1:, :]
112
+ elif t.shape[1] != h*w:
113
+ # Try infer
114
+ n = t.shape[1]
115
+ s = int(n ** 0.5)
116
+ if s*s == n: h = w = s
117
+ else: return t # give up
118
+ t = t # keep as-is
119
+ return t.reshape(t.shape[0], h, w, t.shape[-1]).permute(0, 3, 1, 2)
120
+
121
+ def swin_reshape(t):
122
+ # Swin outputs [B, H, W, C] from last stage
123
+ if t.dim() == 4 and t.shape[-1] > t.shape[1]:
124
+ return t.permute(0, 3, 1, 2)
125
+ return t
126
+
127
+ # ---------- Image picker: one image per class from test split ----------
128
+ M = json.load(open(MANIFEST))
129
+ test_items = M["splits"]["test"]
130
+ per_class = {}
131
+ for rel, lbl in test_items:
132
+ if lbl not in per_class:
133
+ per_class[lbl] = rel
134
+ classes_sorted = [per_class[i] for i in range(10) if i in per_class]
135
+ print(f"Found one test image for each of {len(classes_sorted)} classes")
136
+
137
+ # ---------- Render utility ----------
138
+ def render_grid(model_name, rows, out_path):
139
+ n = len(rows)
140
+ fig, axes = plt.subplots(2, n, figsize=(2.4 * n, 5.4))
141
+ if n == 1: axes = axes.reshape(2, 1)
142
+ for col, (cls_name, raw, heat, pred_name) in enumerate(rows):
143
+ axes[0, col].imshow(raw); axes[0, col].axis("off")
144
+ axes[0, col].set_title(cls_name, fontsize=8)
145
+ axes[1, col].imshow(heat); axes[1, col].axis("off")
146
+ axes[1, col].set_title(f"pred: {pred_name}", fontsize=7)
147
+ fig.suptitle(f"Saliency — {model_name} (v2 weights)", fontsize=13)
148
+ fig.tight_layout()
149
+ fig.savefig(out_path, dpi=130, bbox_inches="tight")
150
+ plt.close(fig)
151
+ print(" ->", out_path)
152
+
153
+ # ---------- Run per model ----------
154
+ def gen_for_model(name, weight_path, builder, target_layer_fn, image_size,
155
+ reshape_transform=None, mode="cam"):
156
+ print(f"\n[{name}] {weight_path.name}")
157
+ if not weight_path.exists():
158
+ print(f" SKIP missing {weight_path}"); return
159
+ if name == "clip_openai":
160
+ model, image_size = builder()
161
+ elif name in ("swin_b", "retfound"):
162
+ model, image_size = builder()
163
+ elif name == "dinov2_l":
164
+ model, image_size = builder()
165
+ else:
166
+ model, _, image_size = builder(name)
167
+ state = torch.load(weight_path, map_location="cpu", weights_only=False)
168
+ if isinstance(state, dict) and "state_dict" in state:
169
+ state = state["state_dict"]
170
+ try:
171
+ model.load_state_dict(state, strict=False)
172
+ except Exception as e:
173
+ print(f" load_state_dict warning: {e}")
174
+ model.to(DEVICE).eval()
175
+ tf = tf_for(image_size)
176
+
177
+ target_layer = target_layer_fn(model) if target_layer_fn else None
178
+ rows = []
179
+ for cls_idx, cls_name in enumerate(CLASS_FULL):
180
+ rel = per_class.get(cls_idx)
181
+ if not rel: continue
182
+ img_path = ROOT / rel
183
+ if not img_path.exists():
184
+ print(f" missing image {img_path}"); continue
185
+ pil = Image.open(img_path).convert("RGB").resize((image_size, image_size))
186
+ raw = np.array(pil).astype(np.float32) / 255.0
187
+ x = tf(pil).unsqueeze(0).to(DEVICE)
188
+
189
+ if mode == "saliency":
190
+ x = x.clone().detach().requires_grad_(True)
191
+ logits = model(x)
192
+ if isinstance(logits, tuple): logits = logits[0]
193
+ pred = int(logits.argmax(1).item())
194
+ score = logits[0, pred]
195
+ model.zero_grad(); score.backward()
196
+ sal = x.grad.detach().abs().max(dim=1)[0][0].cpu().numpy()
197
+ sal = (sal - sal.min()) / (sal.max() - sal.min() + 1e-8)
198
+ heat_rgb = show_cam_on_image(raw, sal, use_rgb=True)
199
+ else:
200
+ cam = GradCAM(model=model, target_layers=[target_layer],
201
+ reshape_transform=reshape_transform)
202
+ gray = cam(input_tensor=x, targets=None)[0]
203
+ heat_rgb = show_cam_on_image(raw, gray, use_rgb=True)
204
+ with torch.no_grad():
205
+ out = model(x)
206
+ if isinstance(out, tuple): out = out[0]
207
+ pred = int(out.argmax(1).item())
208
+ rows.append((cls_name, raw, heat_rgb, CLASS_FULL[pred][:14]))
209
+ render_grid(name, rows, OUT_DIR / f"gradcam_{name}.png")
210
+ del model
211
+ torch.cuda.empty_cache()
212
+
213
+ def _tgt_factory(name):
214
+ if name == "vgg19": return lambda m: m.features[-1]
215
+ if name == "resnet50": return lambda m: m.layer4[-1]
216
+ if name == "resnet101": return lambda m: m.layer4[-1]
217
+ if name == "densenet121": return lambda m: m.features.norm5
218
+ if name == "inception_v3": return lambda m: m.Mixed_7c
219
+ raise ValueError(name)
220
+
221
+ # ---------- CNN models ----------
222
+ for name in ["vgg19", "resnet50", "resnet101", "densenet121", "inception_v3"]:
223
+ w = W_V2 / f"{name}_v2_final.pth"
224
+ gen_for_model(name, w, build_cnn, _tgt_factory(name), 224, mode="cam")
225
+
226
+ # CLIP — saliency
227
+ gen_for_model("clip_openai", W_V2 / "clip_openai_v2_final.pth", build_clip, None, 224, mode="saliency")
228
+
229
+ # Swin-B — use input-gradient saliency for robustness (CAM on hierarchical Swin is fragile)
230
+ gen_for_model("swin_b", W_V3 / "swin_b_v2.pth", build_swin, None, 224, mode="saliency")
231
+
232
+ # DINOv2-L — input-gradient saliency on the wrapper output (avoids hub-build target-layer issues)
233
+ gen_for_model("dinov2_l", W_V3 / "dinov2_l_v2.pth", build_dinov2, None, 224, mode="saliency")
234
+
235
+ # RETFound — input-gradient saliency
236
+ gen_for_model("retfound", W_V3 / "retfound_v2.pth", build_retfound, None, 224, mode="saliency")
237
+
238
+ print("\nALL DONE. PNGs in", OUT_DIR)