Spaces:
Running on Zero
Running on Zero
| import base64 | |
| import io | |
| import numpy as np | |
| import torch | |
| import torch.nn.functional as F | |
| from PIL import Image | |
| from torch import optim | |
| class FeatureVisualizer: | |
| """ | |
| Uses Gradient Ascent to hallucinate an input image that maximally activates a target neuron. | |
| This version uses its OWN hook that preserves gradients (does NOT detach). | |
| """ | |
| def __init__(self, model: torch.nn.Module): | |
| self.model = model | |
| def generate_synthetic_image( | |
| self, | |
| layer_name: str, | |
| channel_idx: int, | |
| steps: int = 200, | |
| lr: float = 0.05, | |
| device: str = "cpu" | |
| ) -> str: | |
| self.model.to(device) | |
| self.model.eval() | |
| # We need our own hook that does NOT detach (so gradients flow) | |
| target_layer = None | |
| for name, module in self.model.named_modules(): | |
| if name == layer_name: | |
| target_layer = module | |
| break | |
| if target_layer is None: | |
| raise ValueError(f"Layer {layer_name} not found") | |
| captured_activation = {} | |
| def grad_hook(module, input, output): | |
| # Store WITHOUT detaching so backward() can flow through | |
| captured_activation['value'] = output | |
| handle = target_layer.register_forward_hook(grad_hook) | |
| # ResNet accepts smaller spatial inputs. Starting at 96px makes the | |
| # interactive feature-visualization endpoint finish on CPU Spaces; | |
| # the result is enlarged for display below. | |
| image_tensor = torch.randn((1, 3, 96, 96), device=device) * 0.01 | |
| image_tensor = image_tensor.requires_grad_(True) | |
| optimizer = optim.Adam([image_tensor], lr=lr, weight_decay=1e-6) | |
| for step in range(steps): | |
| optimizer.zero_grad() | |
| _ = self.model(image_tensor) | |
| acts = captured_activation['value'] | |
| if len(acts.shape) == 4: | |
| target_activation = acts[0, channel_idx, :, :].mean() | |
| else: | |
| target_activation = acts[0, channel_idx] | |
| loss = -target_activation | |
| loss.backward() | |
| optimizer.step() | |
| # Blur every 10 steps to reduce high-frequency noise | |
| if step % 10 == 0: | |
| with torch.no_grad(): | |
| image_tensor.data = F.avg_pool2d( | |
| image_tensor.data, kernel_size=3, stride=1, padding=1 | |
| ) | |
| handle.remove() | |
| # Post-process into viewable image | |
| img_data = image_tensor.detach().cpu().squeeze().numpy() | |
| img_data = np.transpose(img_data, (1, 2, 0)) | |
| for c in range(3): | |
| ch = img_data[:, :, c] | |
| ch_min, ch_max = ch.min(), ch.max() | |
| if ch_max - ch_min > 1e-5: | |
| img_data[:, :, c] = (ch - ch_min) / (ch_max - ch_min) | |
| else: | |
| img_data[:, :, c] = 0.5 | |
| img_uint8 = (np.clip(img_data, 0, 1) * 255).astype(np.uint8) | |
| pil_img = Image.fromarray(img_uint8).resize((512, 512), Image.LANCZOS) | |
| buffered = io.BytesIO() | |
| pil_img.save(buffered, format="PNG") | |
| return base64.b64encode(buffered.getvalue()).decode("utf-8") | |