-
Notifications
You must be signed in to change notification settings - Fork 1.6k
perf(visualize): batch SmoothGrad noise samples with chunked forward #9064
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
03dacb8
15a833d
d5f9e4d
b500631
8cee0f6
b7cacce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,8 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| import math | ||
| import warnings | ||
| from collections.abc import Callable | ||
| from functools import partial | ||
| from typing import Any | ||
|
|
@@ -91,6 +93,12 @@ def get_grad( | |
| ) -> torch.Tensor: | ||
| if x.shape[0] != 1: | ||
| raise ValueError("expect batch size of 1") | ||
| return self._get_grad(x, index, retain_graph=retain_graph, **kwargs) | ||
|
|
||
| def _get_grad( | ||
| self, x: torch.Tensor, index: torch.Tensor | int | None, retain_graph: bool = True, **kwargs: Any | ||
| ) -> torch.Tensor: | ||
| # unguarded gradient computation; ``x`` may carry a batch of noisy copies of a single input | ||
| x.requires_grad = True | ||
|
|
||
| self._model(x, class_idx=index, retain_graph=retain_graph, **kwargs) | ||
|
|
@@ -118,29 +126,86 @@ def __init__( | |
| n_samples: int = 25, | ||
| magnitude: bool = True, | ||
| verbose: bool = True, | ||
| sample_batch_size: int = 1, | ||
| ) -> None: | ||
| super().__init__(model) | ||
| self.stdev_spread = stdev_spread | ||
| self.n_samples = n_samples | ||
| self.magnitude = magnitude | ||
| if isinstance(sample_batch_size, bool) or not isinstance(sample_batch_size, int): | ||
| raise ValueError(f"sample_batch_size must be an int, got {type(sample_batch_size).__name__}.") | ||
| if sample_batch_size < 1: | ||
| raise ValueError(f"sample_batch_size must be >= 1, got {sample_batch_size}.") | ||
| self.sample_batch_size = sample_batch_size | ||
| self.range: Callable | ||
| if verbose and has_trange: | ||
| self.range = partial(trange, desc=f"Computing {self.__class__.__name__}") | ||
| else: | ||
| self.range = range | ||
|
|
||
| def _resolve_index( | ||
| self, x: torch.Tensor, index: torch.Tensor | int | None, **kwargs: Any | ||
| ) -> torch.Tensor | int | None: | ||
| if index is not None: | ||
| if isinstance(index, torch.Tensor): | ||
| # the batched path feeds a forward of size k, so a per-sample index tensor | ||
| # sized for the clean input would select the wrong class per copy | ||
| if index.numel() != 1: | ||
| raise ValueError( | ||
| f"sample_batch_size > 1 expects a single class index, got {index.numel()} elements." | ||
| ) | ||
| return int(index.item()) | ||
|
Comment on lines
+150
to
+157
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Reject non-integral tensor indices.
As per path instructions, examine logical errors and ensure changed definitions have unit tests. 🤖 Prompt for AI AgentsSource: Path instructions |
||
| return index | ||
| # resolve argmax once on clean input, restoring every submodule's mode afterwards | ||
| modules = tuple(self._model.model.modules()) | ||
| training_states = [m.training for m in modules] | ||
| try: | ||
| self._model.model.eval() | ||
| with torch.no_grad(): | ||
| logits = self._model.model(x, **kwargs) | ||
| if isinstance(logits, (list, tuple)): | ||
| logits = logits[0] | ||
| finally: | ||
| for m, training in zip(modules, training_states): | ||
| m.train(training) | ||
| # logits shape (B, C) with B==1 for SmoothGrad input | ||
| resolved: int = int(logits.argmax(dim=1).item()) | ||
| return resolved | ||
|
|
||
| def __call__(self, x: torch.Tensor, index: torch.Tensor | int | None = None, **kwargs: Any) -> torch.Tensor: | ||
| if self.sample_batch_size > 1 and x.shape[0] != 1: | ||
| raise ValueError(f"sample_batch_size > 1 expects an input batch size of 1, got {x.shape[0]}.") | ||
| if self.sample_batch_size > 1 and self._model.model.training: | ||
| warnings.warn( | ||
| "SmoothGrad with sample_batch_size > 1 and model in train mode: " | ||
| "BatchNorm statistics will mix noisy copies.", | ||
| stacklevel=2, | ||
| ) | ||
|
Comment on lines
+178
to
+183
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Remove or correct the BatchNorm warning.
🤖 Prompt for AI Agents |
||
| stdev = (self.stdev_spread * (x.max() - x.min())).item() | ||
| total_gradients = torch.zeros_like(x) | ||
| for _ in self.range(self.n_samples): | ||
| # create noisy image | ||
| noise = torch.normal(0, stdev, size=x.shape, dtype=torch.float32, device=x.device) | ||
| x_plus_noise = x + noise | ||
| x_plus_noise = x_plus_noise.detach() | ||
|
|
||
| # get gradient and accumulate | ||
| grad = self.get_grad(x_plus_noise, index, **kwargs) | ||
| total_gradients += (grad * grad) if self.magnitude else grad | ||
| # resolve index once so all noisy copies use the same class | ||
| resolved_index = self._resolve_index(x, index, **kwargs) if self.sample_batch_size > 1 else index | ||
| n_chunks = math.ceil(self.n_samples / self.sample_batch_size) | ||
| remaining = self.n_samples | ||
| for _ in self.range(n_chunks): | ||
| k = min(self.sample_batch_size, remaining) | ||
| remaining -= k | ||
| if k == 1 and self.sample_batch_size == 1: | ||
| noise = torch.normal(0, stdev, size=x.shape, dtype=torch.float32, device=x.device) | ||
| x_plus_noise = (x + noise).detach() | ||
| grad = self.get_grad(x_plus_noise, index, **kwargs) | ||
| total_gradients += (grad * grad) if self.magnitude else grad | ||
| else: | ||
| # batched path | ||
| noise_shape = (k, *x.shape[1:]) | ||
| noise = torch.normal(0, stdev, size=noise_shape, dtype=torch.float32, device=x.device) | ||
| # x is (1, ...) -> broadcast to (k, ...) | ||
| x_plus_noise = (x + noise).detach() | ||
| grads = self._get_grad(x_plus_noise, resolved_index, **kwargs) | ||
| # grads shape (k, ...) | ||
| if self.magnitude: | ||
| grads = grads * grads | ||
| total_gradients += grads.sum(dim=0, keepdim=True) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Comment on lines
175
to
+208
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Restore module modes around the complete SmoothGrad call.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| # average | ||
| if self.magnitude: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Add Google-style docstrings to the changed definitions.
Document the parameters, return values, and
ValueErrorbehavior for_get_grad,SmoothGrad.__init__,_resolve_index, andSmoothGrad.__call__. Add corresponding purpose and assertion documentation to the new test classes and methods intests/integration/test_vis_gradbased.py.📍 Affects 2 files
monai/visualize/gradient_based.py#L98-L101(this comment)tests/integration/test_vis_gradbased.py#L68-L73🤖 Prompt for AI Agents
Source: Path instructions