Skip to content

Commit

Permalink
refactor: Reorganized package hierarchy (#106)
Browse files Browse the repository at this point in the history
* refactor: Moved torchcam.cams to torchcam.methods

* test: Renamed unittests

* refactor: Updated scripts

* refactor: Updated demo app

* chore: Updated conda recipe

* docs: Udpated documentation and README

* docs: Updated docstrings
  • Loading branch information
frgfm authored Oct 31, 2021
1 parent 8abb3ea commit 5f510c3
Show file tree
Hide file tree
Showing 19 changed files with 73 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .conda/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ test:
# Python imports
imports:
- torchcam
- torchcam.cams
- torchcam.methods
- torchcam.utils
requires:
- python
Expand Down
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ Simple way to leverage the class-specific activation of convolutional layers in

TorchCAM leverages [PyTorch hooking mechanisms](https://pytorch.org/tutorials/beginner/former_torchies/nnft_tutorial.html#forward-and-backward-function-hooks) to seamlessly retrieve all required information to produce the class activation without additional efforts from the user. Each CAM object acts as a wrapper around your model.

You can find the exhaustive list of supported CAM methods in the [documentation](https://frgfm.github.io/torch-cam/cams.html), then use it as follows:
You can find the exhaustive list of supported CAM methods in the [documentation](https://frgfm.github.io/torch-cam/methods.html), then use it as follows:

```python
# Define your model
from torchvision.models import resnet18
model = resnet18(pretrained=True).eval()

# Set your CAM extractor
from torchcam.cams import SmoothGradCAMpp
from torchcam.methods import SmoothGradCAMpp
cam_extractor = SmoothGradCAMpp(model)
```

Expand All @@ -44,7 +44,7 @@ Once your CAM extractor is set, you only need to use your model to infer on your
from torchvision.io.image import read_image
from torchvision.transforms.functional import normalize, resize, to_pil_image
from torchvision.models import resnet18
from torchcam.cams import SmoothGradCAMpp
from torchcam.methods import SmoothGradCAMpp

model = resnet18(pretrained=True).eval()
cam_extractor = SmoothGradCAMpp(model)
Expand Down Expand Up @@ -131,7 +131,7 @@ This project is developed and maintained by the repo owner, but the implementati
<img src="https://github.com/frgfm/torch-cam/releases/download/v0.2.0/video_example_wallaby.gif" /></a>
</p>
<p align="center">
<em>Source: <a href="https://www.youtube.com/watch?v=hZJN5BzKfxk">YouTube video</a> (activation maps created by <a href="https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.LayerCAM">Layer-CAM</a> with a pretrained <a href="https://pytorch.org/vision/stable/models.html#torchvision.models.resnet18">ResNet-18</a>)</em>
<em>Source: <a href="https://www.youtube.com/watch?v=hZJN5BzKfxk">YouTube video</a> (activation maps created by <a href="https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.LayerCAM">Layer-CAM</a> with a pretrained <a href="https://pytorch.org/vision/stable/models.html#torchvision.models.resnet18">ResNet-18</a>)</em>
</p>


Expand Down Expand Up @@ -182,24 +182,24 @@ In the table below, you will find a latency benchmark (forward pass not included

| CAM method | Arch | GPU mean (std) | CPU mean (std) |
| ------------------------------------------------------------ | ------------------ | ------------------ | -------------------- |
| [CAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.CAM) | resnet18 | 0.11ms (0.02ms) | 0.14ms (0.03ms) |
| [GradCAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.GradCAM) | resnet18 | 3.71ms (1.11ms) | 40.66ms (1.82ms) |
| [GradCAMpp](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.GradCAMpp) | resnet18 | 5.21ms (1.22ms) | 41.61ms (3.24ms) |
| [SmoothGradCAMpp](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.SmoothGradCAMpp) | resnet18 | 33.67ms (2.51ms) | 239.27ms (7.85ms) |
| [ScoreCAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.ScoreCAM) | resnet18 | 304.74ms (11.54ms) | 6796.89ms (415.14ms) |
| [SSCAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.SSCAM) | resnet18 | | |
| [ISCAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.ISCAM) | resnet18 | | |
| [XGradCAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.XGradCAM) | resnet18 | 3.78ms (0.96ms) | 40.63ms (2.03ms) |
| [LayerCAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.LayerCAM) | resnet18 | 3.65ms (1.04ms) | 40.91ms (1.79ms) |
| [CAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.CAM) | mobilenet_v3_large | N/A* | N/A* |
| [GradCAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.GradCAM) | mobilenet_v3_large | 8.61ms (1.04ms) | 26.64ms (3.46ms) |
| [GradCAMpp](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.GradCAMpp) | mobilenet_v3_large | 8.83ms (1.29ms) | 25.50ms (3.10ms) |
| [SmoothGradCAMpp](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.SmoothGradCAMpp) | mobilenet_v3_large | 77.38ms (3.83ms) | 156.25ms (4.89ms) |
| [ScoreCAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.ScoreCAM) | mobilenet_v3_large | 35.19ms (2.11ms) | 679.16ms (55.04ms) |
| [SSCAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.SSCAM) | mobilenet_v3_large | | |
| [ISCAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.ISCAM) | mobilenet_v3_large | | |
| [XGradCAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.XGradCAM) | mobilenet_v3_large | 8.41ms (0.98ms) | 24.21ms (2.94ms) |
| [LayerCAM](https://frgfm.github.io/torch-cam/latest/cams.html#torchcam.cams.LayerCAM) | mobilenet_v3_large | 8.02ms (0.95ms) | 25.14ms (3.17ms) |
| [CAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.CAM) | resnet18 | 0.11ms (0.02ms) | 0.14ms (0.03ms) |
| [GradCAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.GradCAM) | resnet18 | 3.71ms (1.11ms) | 40.66ms (1.82ms) |
| [GradCAMpp](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.GradCAMpp) | resnet18 | 5.21ms (1.22ms) | 41.61ms (3.24ms) |
| [SmoothGradCAMpp](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.SmoothGradCAMpp) | resnet18 | 33.67ms (2.51ms) | 239.27ms (7.85ms) |
| [ScoreCAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.ScoreCAM) | resnet18 | 304.74ms (11.54ms) | 6796.89ms (415.14ms) |
| [SSCAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.SSCAM) | resnet18 | | |
| [ISCAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.ISCAM) | resnet18 | | |
| [XGradCAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.XGradCAM) | resnet18 | 3.78ms (0.96ms) | 40.63ms (2.03ms) |
| [LayerCAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.LayerCAM) | resnet18 | 3.65ms (1.04ms) | 40.91ms (1.79ms) |
| [CAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.CAM) | mobilenet_v3_large | N/A* | N/A* |
| [GradCAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.GradCAM) | mobilenet_v3_large | 8.61ms (1.04ms) | 26.64ms (3.46ms) |
| [GradCAMpp](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.GradCAMpp) | mobilenet_v3_large | 8.83ms (1.29ms) | 25.50ms (3.10ms) |
| [SmoothGradCAMpp](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.SmoothGradCAMpp) | mobilenet_v3_large | 77.38ms (3.83ms) | 156.25ms (4.89ms) |
| [ScoreCAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.ScoreCAM) | mobilenet_v3_large | 35.19ms (2.11ms) | 679.16ms (55.04ms) |
| [SSCAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.SSCAM) | mobilenet_v3_large | | |
| [ISCAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.ISCAM) | mobilenet_v3_large | | |
| [XGradCAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.XGradCAM) | mobilenet_v3_large | 8.41ms (0.98ms) | 24.21ms (2.94ms) |
| [LayerCAM](https://frgfm.github.io/torch-cam/latest/methods.html#torchcam.methods.LayerCAM) | mobilenet_v3_large | 8.02ms (0.95ms) | 25.14ms (3.17ms) |

**The base CAM method cannot work with architectures that have multiple fully-connected layers*

Expand Down
7 changes: 4 additions & 3 deletions demo/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
from torchvision import models
from torchvision.transforms.functional import normalize, resize, to_pil_image, to_tensor

from torchcam import cams
from torchcam import methods
from torchcam.methods._utils import locate_candidate_layer
from torchcam.utils import overlay_mask

CAM_METHODS = ["CAM", "GradCAM", "GradCAMpp", "SmoothGradCAMpp", "ScoreCAM", "SSCAM", "ISCAM", "XGradCAM", "LayerCAM"]
Expand Down Expand Up @@ -56,12 +57,12 @@ def main():
if tv_model is not None:
with st.spinner('Loading model...'):
model = models.__dict__[tv_model](pretrained=True).eval()
default_layer = cams.utils.locate_candidate_layer(model, (3, 224, 224))
default_layer = locate_candidate_layer(model, (3, 224, 224))

target_layer = st.sidebar.text_input("Target layer", default_layer)
cam_method = st.sidebar.selectbox("CAM method", CAM_METHODS)
if cam_method is not None:
cam_extractor = cams.__dict__[cam_method](
cam_extractor = methods.__dict__[cam_method](
model,
target_layer=target_layer.split("+") if len(target_layer) > 0 else None
)
Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Gradient-based methods
:caption: Package Reference
:hidden:

cams
methods
utils


Expand Down
6 changes: 3 additions & 3 deletions docs/source/cams.rst → docs/source/methods.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
torchcam.cams
=============
torchcam.methods
================


.. currentmodule:: torchcam.cams
.. currentmodule:: torchcam.methods


Class activation map
Expand Down
2 changes: 1 addition & 1 deletion docs/source/notebooks/quicktour.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Basic usage
>>> from torchvision.models import resnet18
>>> from torchvision.transforms.functional import normalize, resize, to_pil_image
>>>
>>> from torchcam.cams import SmoothGradCAMpp, LayerCAM
>>> from torchcam.methods import SmoothGradCAMpp, LayerCAM
>>> from torchcam.utils import overlay_mask
.. code-block:: python
Expand Down
8 changes: 4 additions & 4 deletions scripts/cam_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from torchvision import models
from torchvision.transforms.functional import normalize, resize, to_pil_image, to_tensor

from torchcam import cams
from torchcam import methods
from torchcam.utils import overlay_mask


Expand All @@ -44,16 +44,16 @@ def main(args):
[0.485, 0.456, 0.406], [0.229, 0.224, 0.225]).to(device=device)

if isinstance(args.method, str):
methods = [args.method]
cam_methods = [args.method]
else:
methods = [
cam_methods = [
'CAM',
'GradCAM', 'GradCAMpp', 'SmoothGradCAMpp',
'ScoreCAM', 'SSCAM', 'ISCAM',
'XGradCAM', 'LayerCAM'
]
# Hook the corresponding layer in the model
cam_extractors = [cams.__dict__[name](model, enable_hooks=False) for name in methods]
cam_extractors = [methods.__dict__[name](model, enable_hooks=False) for name in cam_methods]

# Homogenize number of elements in each row
num_cols = math.ceil((len(cam_extractors) + 1) / args.rows)
Expand Down
2 changes: 1 addition & 1 deletion scripts/eval_latency.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import torch
from torchvision import models

from torchcam import cams as methods
from torchcam import methods


def main(args):
Expand Down
12 changes: 6 additions & 6 deletions test/test_cams_cam.py → test/test_methods_activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@
import torch
from torchvision.models import mobilenet_v2

from torchcam.cams import cam
from torchcam.methods import activation


def test_base_cam_constructor(mock_img_model):
model = mobilenet_v2(pretrained=False).eval()
# Check that multiple target layers is disabled for base CAM
with pytest.raises(ValueError):
_ = cam.CAM(model, ['classifier.1', 'classifier.2'])
_ = activation.CAM(model, ['classifier.1', 'classifier.2'])

# FC layer checks
with pytest.raises(TypeError):
_ = cam.CAM(model, fc_layer=3)
_ = activation.CAM(model, fc_layer=3)


def _verify_cam(activation_map, output_size):
Expand Down Expand Up @@ -52,7 +52,7 @@ def test_img_cams(cam_name, target_layer, fc_layer, num_samples, output_size, mo

target_layer = target_layer(model) if callable(target_layer) else target_layer
# Hook the corresponding layer in the model
extractor = cam.__dict__[cam_name](model, target_layer, **kwargs)
extractor = activation.__dict__[cam_name](model, target_layer, **kwargs)

with torch.no_grad():
scores = model(mock_img_tensor)
Expand All @@ -61,7 +61,7 @@ def test_img_cams(cam_name, target_layer, fc_layer, num_samples, output_size, mo


def test_cam_conv1x1(mock_fullyconv_model):
extractor = cam.CAM(mock_fullyconv_model, fc_layer='1')
extractor = activation.CAM(mock_fullyconv_model, fc_layer='1')
with torch.no_grad():
scores = mock_fullyconv_model(torch.rand((1, 3, 32, 32)))
# Use the hooked data to compute activation map
Expand All @@ -85,7 +85,7 @@ def test_video_cams(cam_name, target_layer, num_samples, output_size, mock_video
kwargs['num_samples'] = num_samples

# Hook the corresponding layer in the model
extractor = cam.__dict__[cam_name](model, target_layer, **kwargs)
extractor = activation.__dict__[cam_name](model, target_layer, **kwargs)

with torch.no_grad():
scores = model(mock_video_tensor)
Expand Down
2 changes: 1 addition & 1 deletion test/test_cams_core.py → test/test_methods_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest
import torch

from torchcam.cams import core
from torchcam.methods import core


def test_cam_constructor(mock_img_model):
Expand Down
20 changes: 10 additions & 10 deletions test/test_cams_gradcam.py → test/test_methods_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from torch import nn
from torchvision.models import mobilenet_v2

from torchcam.cams import gradcam
from torchcam.methods import gradient


def _verify_cam(activation_map, output_size):
Expand All @@ -34,7 +34,7 @@ def test_img_cams(cam_name, target_layer, output_size, mock_img_tensor):

target_layer = target_layer(model) if callable(target_layer) else target_layer
# Hook the corresponding layer in the model
extractor = gradcam.__dict__[cam_name](model, target_layer)
extractor = gradient.__dict__[cam_name](model, target_layer)

scores = model(mock_img_tensor)
# Use the hooked data to compute activation map
Expand All @@ -52,7 +52,7 @@ def test_img_cams(cam_name, target_layer, output_size, mock_img_tensor):
)

# Hook before the inplace ops
extractor = gradcam.__dict__[cam_name](model, '2')
extractor = gradient.__dict__[cam_name](model, '2')
scores = model(mock_img_tensor)
# Use the hooked data to compute activation map
_verify_cam(extractor(scores[0].argmax().item(), scores)[0], (224, 224))
Expand All @@ -71,7 +71,7 @@ def test_img_cams(cam_name, target_layer, output_size, mock_img_tensor):
def test_video_cams(cam_name, target_layer, output_size, mock_video_model, mock_video_tensor):
model = mock_video_model.eval()
# Hook the corresponding layer in the model
extractor = gradcam.__dict__[cam_name](model, target_layer)
extractor = gradient.__dict__[cam_name](model, target_layer)

scores = model(mock_video_tensor)
# Use the hooked data to compute activation map
Expand All @@ -82,32 +82,32 @@ def test_smoothgradcampp_repr():
model = mobilenet_v2(pretrained=False).eval()

# Hook the corresponding layer in the model
extractor = gradcam.SmoothGradCAMpp(model, 'features.18.0')
extractor = gradient.SmoothGradCAMpp(model, 'features.18.0')

assert repr(extractor) == "SmoothGradCAMpp(target_layer=['features.18.0'], num_samples=4, std=0.3)"


def test_layercam_fuse_cams(mock_img_model):

with pytest.raises(TypeError):
gradcam.LayerCAM.fuse_cams(torch.zeros((3, 32, 32)))
gradient.LayerCAM.fuse_cams(torch.zeros((3, 32, 32)))

with pytest.raises(ValueError):
gradcam.LayerCAM.fuse_cams([])
gradient.LayerCAM.fuse_cams([])

cams = [torch.rand((32, 32)), torch.rand((16, 16))]

# Single CAM
assert torch.equal(cams[0], gradcam.LayerCAM.fuse_cams(cams[:1]))
assert torch.equal(cams[0], gradient.LayerCAM.fuse_cams(cams[:1]))

# Fusion
cam = gradcam.LayerCAM.fuse_cams(cams)
cam = gradient.LayerCAM.fuse_cams(cams)
assert isinstance(cam, torch.Tensor)
assert cam.ndim == cams[0].ndim
assert cam.shape == (32, 32)

# Specify target shape
cam = gradcam.LayerCAM.fuse_cams(cams, (16, 16))
cam = gradient.LayerCAM.fuse_cams(cams, (16, 16))
assert isinstance(cam, torch.Tensor)
assert cam.ndim == cams[0].ndim
assert cam.shape == (16, 16)
10 changes: 5 additions & 5 deletions test/test_cams_utils.py → test/test_methods_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@

from torchvision.models import resnet18

from torchcam.cams import utils
from torchcam.methods import _utils


def test_locate_candidate_layer(mock_img_model):
# ResNet-18
mod = resnet18().eval()
assert utils.locate_candidate_layer(mod) == 'layer4'
assert _utils.locate_candidate_layer(mod) == 'layer4'

# Custom model
mod = mock_img_model.train()

assert utils.locate_candidate_layer(mod) == '0.3'
assert _utils.locate_candidate_layer(mod) == '0.3'
# Check that the model is switched back to its origin mode afterwards
assert mod.training

Expand All @@ -25,8 +25,8 @@ def test_locate_linear_layer(mock_img_model):

# ResNet-18
mod = resnet18().eval()
assert utils.locate_linear_layer(mod) == 'fc'
assert _utils.locate_linear_layer(mod) == 'fc'

# Custom model
mod = mock_img_model
assert utils.locate_linear_layer(mod) == '2'
assert _utils.locate_linear_layer(mod) == '2'
2 changes: 1 addition & 1 deletion torchcam/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from torchcam import cams, utils
from torchcam import methods, utils

try:
from .version import __version__ # noqa: F401
Expand Down
3 changes: 0 additions & 3 deletions torchcam/cams/__init__.py

This file was deleted.

2 changes: 2 additions & 0 deletions torchcam/methods/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .activation import *
from .gradient import *
File renamed without changes.
Loading

0 comments on commit 5f510c3

Please sign in to comment.