signal-grad-cam 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of signal-grad-cam might be problematic. Click here for more details.
- signal_grad_cam/__init__.py +3 -0
- signal_grad_cam/cam_builder.py +1032 -0
- signal_grad_cam/pytorch_cam_builder.py +314 -0
- signal_grad_cam/tensorflow_cam_builder.py +286 -0
- signal_grad_cam-0.0.1.dist-info/LICENSE +21 -0
- signal_grad_cam-0.0.1.dist-info/METADATA +228 -0
- signal_grad_cam-0.0.1.dist-info/RECORD +9 -0
- signal_grad_cam-0.0.1.dist-info/WHEEL +5 -0
- signal_grad_cam-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Import dependencies
|
|
2
|
+
import numpy as np
|
|
3
|
+
import torch
|
|
4
|
+
import torch.nn as nn
|
|
5
|
+
from typing import Callable, List, Tuple, Dict, Any
|
|
6
|
+
|
|
7
|
+
from signal_grad_cam import CamBuilder
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Class
|
|
11
|
+
class TorchCamBuilder(CamBuilder):
|
|
12
|
+
"""
|
|
13
|
+
Represents a PyTorch Class Activation Map (CAM) builder, supporting multiple methods such as Grad-CAM and HiResCAM.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, model: nn.Module | Any, transform_fn: Callable = None, class_names: List[str] = None,
|
|
17
|
+
time_axs: int = 1, input_transposed: bool = False, ignore_channel_dim: bool = False,
|
|
18
|
+
model_output_index: int = None, extend_search: bool = False, use_gpu: bool = False,
|
|
19
|
+
padding_dim: int = None, seed: int = 11):
|
|
20
|
+
"""
|
|
21
|
+
Initializes the TorchCamBuilder class. The constructor also displays, if present and retrievable, the 1D- and
|
|
22
|
+
2D-convolutional layers in the network, as well as the final Sigmoid/Softmax activation. Additionally, the CAM
|
|
23
|
+
algorithms available for generating the explanations are shown.
|
|
24
|
+
|
|
25
|
+
:param model: (mandatory) A torch.nn.Module or any object (with PyTorch layers among its attributes)
|
|
26
|
+
representing a convolutional neural network model to be explained. Unconventional models should always be
|
|
27
|
+
set to inference mode before being provided as inputs.
|
|
28
|
+
:param transform_fn: (optional, default is None) A callable function to preprocess np.ndarray data before model
|
|
29
|
+
evaluation. This function is also expected to convert data into either PyTorch or TensorFlow tensors.
|
|
30
|
+
:param class_names: (optional, default is None) A list of strings where each string represents the name of an
|
|
31
|
+
output class.
|
|
32
|
+
:param time_axs: (optional, default is 1) An integer index indicating whether the input signal's time axis is
|
|
33
|
+
represented as the first or second dimension of the input array.
|
|
34
|
+
:param input_transposed: (optional, default is False) A boolean indicating whether the input array is transposed
|
|
35
|
+
during model inference, either by the model itself or by the preprocessing function.
|
|
36
|
+
:param ignore_channel_dim: (optional, default is False) A boolean indicating whether to ignore the channel
|
|
37
|
+
dimension. This is useful when the model expects inputs without a singleton channel dimension.
|
|
38
|
+
:param model_output_index: (optional, default is None) An integer index specifying which of the model's outputs
|
|
39
|
+
represents output scores (or probabilities). If there is only one output, this argument can be ignored.
|
|
40
|
+
:param extend_search: (optional, default is False) A boolean flag indicating whether to deepend the search for
|
|
41
|
+
candidate layers. It should be set true if no convolutional layer was found.
|
|
42
|
+
:param use_gpu: (optional, default is False) A boolean flag indicating whether to use GPU for data processing,
|
|
43
|
+
if GPU is available.
|
|
44
|
+
:param padding_dim: (optional, default is None) An integer specifying the maximum length along the time axis to
|
|
45
|
+
which each item will be padded for batching.
|
|
46
|
+
:param seed: (optional, default is 11) An integer seed for random number generators, used to ensure
|
|
47
|
+
reproducibility during model evaluation.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# Initialize attributes
|
|
51
|
+
super(TorchCamBuilder, self).__init__(model=model, transform_fn=transform_fn, class_names=class_names,
|
|
52
|
+
time_axs=time_axs, input_transposed=input_transposed,
|
|
53
|
+
ignore_channel_dim=ignore_channel_dim,
|
|
54
|
+
model_output_index=model_output_index, extend_search=extend_search,
|
|
55
|
+
padding_dim=padding_dim, seed=seed)
|
|
56
|
+
|
|
57
|
+
# Set seeds
|
|
58
|
+
torch.manual_seed(seed)
|
|
59
|
+
torch.cuda.manual_seed_all(seed)
|
|
60
|
+
torch.use_deterministic_algorithms(True)
|
|
61
|
+
torch.backends.cudnn.deterministic = True
|
|
62
|
+
torch.backends.cudnn.benchmark = False
|
|
63
|
+
|
|
64
|
+
# Check for the evaluation mode method
|
|
65
|
+
if hasattr(model, "eval"):
|
|
66
|
+
self.model.eval()
|
|
67
|
+
else:
|
|
68
|
+
print("Your PyTorch model has no 'eval' method. Please verify that the networks has been set to "
|
|
69
|
+
"evaluation mode before the TorchCamBuilder initialization.")
|
|
70
|
+
self.use_gpu = use_gpu
|
|
71
|
+
|
|
72
|
+
# Assign the default transform function
|
|
73
|
+
if transform_fn is None:
|
|
74
|
+
self.transform_fn = self.__default_transform_fn
|
|
75
|
+
|
|
76
|
+
def _get_layers_pool(self, show: bool = False, extend_search: bool = False) \
|
|
77
|
+
-> Dict[str, torch.nn.Module | Any]:
|
|
78
|
+
"""
|
|
79
|
+
Retrieves a dictionary containing all the available PyTorch layers (or instance attributes), with the layer (or
|
|
80
|
+
attribute) names used as keys.
|
|
81
|
+
|
|
82
|
+
:param show: (optional, default is False) A boolean flag indicating whether to display the retrieved layers
|
|
83
|
+
along with their names.
|
|
84
|
+
:param extend_search: (optional, default is False) A boolean flag indicating whether to deepend the search for
|
|
85
|
+
candidate layers. It should be set true if no convolutional layer was found.
|
|
86
|
+
|
|
87
|
+
:return:
|
|
88
|
+
- layers_pool: A dictionary storing the model's PyTorch layers (or instance attributes),
|
|
89
|
+
with layer (or attribute) names as keys.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
if hasattr(self.model, "named_modules"):
|
|
93
|
+
layers_pool = dict(self.model.named_modules())
|
|
94
|
+
if show:
|
|
95
|
+
for name, layer in layers_pool.items():
|
|
96
|
+
self._show_layer(name, layer)
|
|
97
|
+
else:
|
|
98
|
+
layers_pool = super()._get_layers_pool(show=show)
|
|
99
|
+
|
|
100
|
+
if extend_search:
|
|
101
|
+
layers_pool.update(super()._get_layers_pool(show=show))
|
|
102
|
+
|
|
103
|
+
return layers_pool
|
|
104
|
+
|
|
105
|
+
def _show_layer(self, name: str, layer: nn.Module | Any, potential: bool = False):
|
|
106
|
+
"""
|
|
107
|
+
Displays a single available layer (or instance attribute) in the model, along with its corresponding name.
|
|
108
|
+
|
|
109
|
+
:param name: (mandatory) A string representing the name of the layer or attribute.
|
|
110
|
+
:param layer: (mandatory) A PyTorch layer or an instance attribute in the model.
|
|
111
|
+
:param potential: (optional, default is False) A flag indicating whether the object displayed is potentially
|
|
112
|
+
a layer (i.e., a generic instance attribute, not guaranteed to be a layer).
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
if (isinstance(layer, nn.Conv1d) or isinstance(layer, nn.Conv2d) or
|
|
116
|
+
isinstance(layer, nn.Softmax) or isinstance(layer, nn.Sigmoid)):
|
|
117
|
+
super()._show_layer(name, layer, potential=potential)
|
|
118
|
+
|
|
119
|
+
def _create_raw_batched_cams(self, data_list: List[np.array], target_class: int, target_layer: nn.Module,
|
|
120
|
+
explainer_type: str, softmax_final: bool) -> Tuple[List[np.ndarray], np.ndarray]:
|
|
121
|
+
"""
|
|
122
|
+
Retrieves raw CAMs from an input data list based on the specified settings (defined by algorithm, target layer,
|
|
123
|
+
and target class). Additionally, it returns the class probabilities predicted by the model.
|
|
124
|
+
|
|
125
|
+
:param data_list: (mandatory) A list of np.ndarrays to be explained, representing either a signal or an image.
|
|
126
|
+
:param target_class: (mandatory) An integer representing the target class for the explanation.
|
|
127
|
+
:param target_layer: (mandatory) A string representing the target layer for the explanation. This string should
|
|
128
|
+
identify either PyTorch named modules or it should be a class dictionary key, used to retrieve the layer
|
|
129
|
+
from the class attributes.
|
|
130
|
+
:param explainer_type: (mandatory) A string representing the desired algorithm for the explanation. This string
|
|
131
|
+
should identify one of the CAM algorithms allowed, as listed by the class constructor.
|
|
132
|
+
:param softmax_final: (mandatory) A boolean indicating whether the network terminates with a Sigmoid/Softmax
|
|
133
|
+
activation function.
|
|
134
|
+
|
|
135
|
+
:return:
|
|
136
|
+
- cam_list: A list of np.ndarray containing CAMs for each item in the input data list, corresponding to the
|
|
137
|
+
given setting (defined by algorithm, target layer, and target class).
|
|
138
|
+
- target_probs: A np.ndarray, representing the inferred class probabilities for each item in the input list.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
# Register hooks
|
|
142
|
+
_ = target_layer.register_forward_hook(self.__get_activation_forward_hook, prepend=False)
|
|
143
|
+
_ = target_layer.register_forward_hook(self.__get_gradient_forward_hook, prepend=False)
|
|
144
|
+
|
|
145
|
+
# Data batching
|
|
146
|
+
if not isinstance(data_list[0], torch.Tensor):
|
|
147
|
+
data_list = [torch.Tensor(x) for x in data_list]
|
|
148
|
+
if self.padding_dim is not None:
|
|
149
|
+
padded_data_list = []
|
|
150
|
+
for item in data_list:
|
|
151
|
+
pad_size = self.padding_dim - item.shape[self.time_axs]
|
|
152
|
+
if not self.time_axs:
|
|
153
|
+
zeros = torch.zeros((pad_size, item.shape[1]), dtype=item.dtype,
|
|
154
|
+
device=item.device)
|
|
155
|
+
else:
|
|
156
|
+
zeros = torch.zeros((item.shape[0], pad_size), dtype=item.dtype,
|
|
157
|
+
device=item.device)
|
|
158
|
+
padded_data_list.append(torch.cat((item, zeros), dim=self.time_axs))
|
|
159
|
+
data_list = padded_data_list
|
|
160
|
+
|
|
161
|
+
is_2d_layer = self._is_2d_layer(target_layer)
|
|
162
|
+
if not self.ignore_channel_dim and (is_2d_layer and len(data_list[0].shape) == 2 or not is_2d_layer
|
|
163
|
+
and len(data_list[0].shape) == 1):
|
|
164
|
+
data_list = [x.unsqueeze(0) for x in data_list]
|
|
165
|
+
data_batch = torch.stack(data_list)
|
|
166
|
+
|
|
167
|
+
outputs = self.model(data_batch)
|
|
168
|
+
if isinstance(outputs, tuple):
|
|
169
|
+
outputs = outputs[self.model_output_index]
|
|
170
|
+
|
|
171
|
+
if softmax_final:
|
|
172
|
+
# Approximate Softmax inversion formula logit = log(prob) + constant, as the constant is negligible
|
|
173
|
+
# during derivation
|
|
174
|
+
target_scores = torch.log(outputs)
|
|
175
|
+
target_probs = outputs
|
|
176
|
+
else:
|
|
177
|
+
target_scores = outputs
|
|
178
|
+
target_probs = torch.softmax(target_scores, dim=1)
|
|
179
|
+
|
|
180
|
+
target_probs = target_probs[:, target_class].cpu().detach().numpy()
|
|
181
|
+
|
|
182
|
+
cam_list = []
|
|
183
|
+
for i in range(len(data_list)):
|
|
184
|
+
self.model.zero_grad()
|
|
185
|
+
target_score = target_scores[i, target_class]
|
|
186
|
+
target_score.backward(retain_graph=True)
|
|
187
|
+
|
|
188
|
+
if explainer_type == "HiResCAM":
|
|
189
|
+
cam = self._get_hirescam_map(is_2d_layer=is_2d_layer, batch_idx=i)
|
|
190
|
+
else:
|
|
191
|
+
cam = self._get_gradcam_map(is_2d_layer=is_2d_layer, batch_idx=i)
|
|
192
|
+
cam_list.append(cam.cpu().detach().numpy())
|
|
193
|
+
|
|
194
|
+
return cam_list, target_probs
|
|
195
|
+
|
|
196
|
+
def _get_gradcam_map(self, is_2d_layer: bool, batch_idx: int) -> torch.Tensor:
|
|
197
|
+
"""
|
|
198
|
+
Compute the CAM using the vanilla Gradient-weighted Class Activation Mapping (Grad-CAM) algorithm.
|
|
199
|
+
|
|
200
|
+
:param is_2d_layer: (mandatory) A boolean indicating whether the target layers 2D-convolutional layer.
|
|
201
|
+
:param batch_idx: (mandatory) The index corresponding to the i-th selected item within the original input data
|
|
202
|
+
list.
|
|
203
|
+
|
|
204
|
+
:return: cam: A PyTorch tensor representing the Class Activation Map (CAM) for the batch_idx-th input, built
|
|
205
|
+
with the Grad-CAM algorithm.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
if is_2d_layer:
|
|
209
|
+
dim_mean = (1, 2)
|
|
210
|
+
else:
|
|
211
|
+
dim_mean = 1
|
|
212
|
+
weights = torch.mean(self.gradients[batch_idx], dim=dim_mean)
|
|
213
|
+
activations = self.activations[batch_idx].clone()
|
|
214
|
+
|
|
215
|
+
for i in range(self.activations.shape[1]):
|
|
216
|
+
if is_2d_layer:
|
|
217
|
+
activations[i, :, :] *= weights[i]
|
|
218
|
+
else:
|
|
219
|
+
activations[i, :] *= weights[i]
|
|
220
|
+
|
|
221
|
+
cam = torch.sum(activations, dim=0)
|
|
222
|
+
cam = torch.relu(cam)
|
|
223
|
+
return cam
|
|
224
|
+
|
|
225
|
+
def _get_hirescam_map(self, is_2d_layer: bool, batch_idx: int) -> torch.Tensor:
|
|
226
|
+
"""
|
|
227
|
+
Compute the CAM using the High-Resolution Class Activation Mapping (HiResCAM) algorithm.
|
|
228
|
+
|
|
229
|
+
:param is_2d_layer: (mandatory) A boolean indicating whether the target layers 2D-convolutional layer.
|
|
230
|
+
:param batch_idx: (mandatory) The index corresponding to the i-th selected item within the original input data
|
|
231
|
+
list.
|
|
232
|
+
|
|
233
|
+
:return: cam: A PyTorch tensor representing the Class Activation Map (CAM) for the batch_idx-th input, built
|
|
234
|
+
with the HiResCAM algorithm.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
activations = self.activations[batch_idx].clone()
|
|
238
|
+
gradients = self.gradients[batch_idx]
|
|
239
|
+
|
|
240
|
+
for i in range(self.activations.shape[1]):
|
|
241
|
+
if is_2d_layer:
|
|
242
|
+
activations[i, :, :] *= gradients[i, :, :]
|
|
243
|
+
else:
|
|
244
|
+
activations[i, :] *= gradients[i, :]
|
|
245
|
+
|
|
246
|
+
cam = torch.sum(activations, dim=0)
|
|
247
|
+
cam = torch.relu(cam)
|
|
248
|
+
return cam
|
|
249
|
+
|
|
250
|
+
def __get_activation_forward_hook(self, layer: nn.Module, inputs: Tuple[torch.Tensor, ...], outputs: torch.Tensor) \
|
|
251
|
+
-> None:
|
|
252
|
+
"""
|
|
253
|
+
Defines the forward hook function for capturing intermediate activations during model inference.
|
|
254
|
+
|
|
255
|
+
:param layer: (mandatory) The target PyTorch layer where the hook is attached.
|
|
256
|
+
:param inputs: (mandatory) A tuple containing the input tensors received by the layer.
|
|
257
|
+
:param outputs: (mandatory) The output tensor produced by the layer after applying its operations.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
self.activations = outputs
|
|
261
|
+
|
|
262
|
+
def __get_gradient_forward_hook(self, layer: nn.Module, inputs: Tuple[torch.Tensor, ...], outputs: torch.Tensor) \
|
|
263
|
+
-> None:
|
|
264
|
+
"""
|
|
265
|
+
Defines the forward hook function for capturing intermediate gradients during model inference.
|
|
266
|
+
|
|
267
|
+
:param layer: (mandatory) The target PyTorch layer where the hook is attached.
|
|
268
|
+
:param inputs: (mandatory) A tuple containing the input tensors received by the layer.
|
|
269
|
+
:param outputs: (mandatory) The output tensor produced by the layer after applying its operations.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
outputs.register_hook(self.__store_grad)
|
|
273
|
+
|
|
274
|
+
def __store_grad(self, gradients: torch.Tensor) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Captures intermediate gradients during backpropagation.
|
|
277
|
+
|
|
278
|
+
:param gradients: (mandatory) A tensor containing the gradients of the layer's outputs, computed during
|
|
279
|
+
backpropagation.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
self.gradients = gradients
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _is_2d_layer(target_layer: nn.Module) -> bool:
|
|
286
|
+
"""
|
|
287
|
+
Evaluates whether the target layer is a 2D-convolutional layer.
|
|
288
|
+
|
|
289
|
+
:param target_layer: (mandatory) A PyTorch module.
|
|
290
|
+
|
|
291
|
+
:return:
|
|
292
|
+
- is_2d_layer: A boolean indicating whether the target layers 2D-convolutional layer.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
if isinstance(target_layer, nn.Conv1d):
|
|
296
|
+
is_2d_layer = False
|
|
297
|
+
elif isinstance(target_layer, nn.Conv2d):
|
|
298
|
+
is_2d_layer = True
|
|
299
|
+
else:
|
|
300
|
+
is_2d_layer = CamBuilder._is_2d_layer(target_layer)
|
|
301
|
+
return is_2d_layer
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def __default_transform_fn(np_input: np.ndarray) -> torch.Tensor:
|
|
305
|
+
"""
|
|
306
|
+
Converts a NumPy array to a PyTorch tensor with float type.
|
|
307
|
+
|
|
308
|
+
:param np_input: (mandatory) A NumPy array representing the input data.
|
|
309
|
+
|
|
310
|
+
:return: A PyTorch tensor converted from the input NumPy array, with float data type.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
torch_input = torch.from_numpy(np_input).float()
|
|
314
|
+
return torch_input
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Import dependencies
|
|
2
|
+
import os
|
|
3
|
+
os.environ["PYTHONHASHSEED"] = "11"
|
|
4
|
+
os.environ["TF_DETERMINISTIC_OPS"] = "1"
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import keras
|
|
8
|
+
import tensorflow as tf
|
|
9
|
+
from typing import Callable, List, Tuple, Dict, Any
|
|
10
|
+
|
|
11
|
+
from signal_grad_cam import CamBuilder
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Class
|
|
15
|
+
class TfCamBuilder(CamBuilder):
|
|
16
|
+
"""
|
|
17
|
+
Represents a TensorFlow/Keras Class Activation Map (CAM) builder, supporting multiple methods such as Grad-CAM and
|
|
18
|
+
HiResCAM.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, model: tf.keras.Model | Any, transform_fn: Callable = None, class_names: List[str] = None,
|
|
22
|
+
time_axs: int = 1, input_transposed: bool = False, ignore_channel_dim: bool = False,
|
|
23
|
+
model_output_index: int = None, extend_search: bool = False, padding_dim: int = None, seed: int = 11):
|
|
24
|
+
"""
|
|
25
|
+
Initializes the TfCamBuilder class. The constructor also displays, if present and retrievable, the 1D- and
|
|
26
|
+
2D-convolutional layers in the network, as well as the final Sigmoid/Softmax activation. Additionally, the CAM
|
|
27
|
+
algorithms available for generating the explanations are shown.
|
|
28
|
+
|
|
29
|
+
:param model: (mandatory) A tf.keras.Model or any object (with TensorFlow/Keras layers among its attributes)
|
|
30
|
+
representing a convolutional neural network model to be explained. Unconventional models should always be
|
|
31
|
+
set to inference mode before being provided as inputs.
|
|
32
|
+
:param transform_fn: (optional, default is None) A callable function to preprocess np.ndarray data before model
|
|
33
|
+
evaluation. This function is also expected to convert data into either PyTorch or TensorFlow tensors.
|
|
34
|
+
:param class_names: (optional, default is None) A list of strings where each string represents the name of an
|
|
35
|
+
output class.
|
|
36
|
+
:param time_axs: (optional, default is 1) An integer index indicating whether the input signal's time axis is
|
|
37
|
+
represented as the first or second dimension of the input array.
|
|
38
|
+
:param input_transposed: (optional, default is False) A boolean indicating whether the input array is transposed
|
|
39
|
+
during model inference, either by the model itself or by the preprocessing function.
|
|
40
|
+
:param ignore_channel_dim: (optional, default is False) A boolean indicating whether to ignore the channel
|
|
41
|
+
dimension. This is useful when the model expects inputs without a singleton channel dimension.
|
|
42
|
+
:param model_output_index: (optional, default is None) An integer index specifying which of the model's outputs
|
|
43
|
+
represents output scores (or probabilities). If there is only one output, this argument can be ignored.
|
|
44
|
+
:param extend_search: (optional, default is False) A boolean flag indicating whether to deepend the search for
|
|
45
|
+
candidate layers. It should be set true if no convolutional layer was found.
|
|
46
|
+
:param padding_dim: (optional, default is None) An integer specifying the maximum length along the time axis to
|
|
47
|
+
which each item will be padded for batching.
|
|
48
|
+
:param seed: (optional, default is 11) An integer seed for random number generators, used to ensure
|
|
49
|
+
reproducibility during model evaluation.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# Initialize attributes
|
|
53
|
+
super(TfCamBuilder, self).__init__(model=model, transform_fn=transform_fn, class_names=class_names,
|
|
54
|
+
time_axs=time_axs, input_transposed=input_transposed,
|
|
55
|
+
ignore_channel_dim=ignore_channel_dim, model_output_index=model_output_index,
|
|
56
|
+
extend_search=extend_search, padding_dim=padding_dim, seed=seed)
|
|
57
|
+
|
|
58
|
+
# Set seeds
|
|
59
|
+
tf.random.set_seed(seed)
|
|
60
|
+
|
|
61
|
+
# Check for input/output attributes
|
|
62
|
+
if not hasattr(model, "inputs"):
|
|
63
|
+
print("Your TensorFlow/Keras model has no attribute 'inputs'. Ensure it is built or loaded correctly, or\n"
|
|
64
|
+
"provide a different one. If the model contains a 'Sequential' attribute, that Sequential object may\n"
|
|
65
|
+
"be a suitable candidate for an input model.")
|
|
66
|
+
elif not hasattr(model, "output"):
|
|
67
|
+
print("Your TensorFlow/Keras model has no attribute 'output'. Ensure it is built or loaded correctly, or\n"
|
|
68
|
+
"provide a different one. If the model contains a 'Sequential' attribute, that Sequential object may\n"
|
|
69
|
+
"be a suitable candidate for an input model.")
|
|
70
|
+
|
|
71
|
+
def _get_layers_pool(self, show: bool = False, extend_search: bool = False) \
|
|
72
|
+
-> Dict[str, tf.keras.layers.Layer | Any]:
|
|
73
|
+
"""
|
|
74
|
+
Retrieves a dictionary containing all the available TensorFlow/Keras layers (or instance attributes), with the
|
|
75
|
+
layer (or attribute) names used as keys.
|
|
76
|
+
|
|
77
|
+
:param show: (optional, default is False) A boolean flag indicating whether to display the retrieved layers
|
|
78
|
+
along with their names.
|
|
79
|
+
:param extend_search: (optional, default is False) A boolean flag indicating whether to deepend the search for
|
|
80
|
+
candidate layers. It should be set true if no convolutional layer was found.
|
|
81
|
+
|
|
82
|
+
:return:
|
|
83
|
+
- layers_pool: A dictionary storing the model's TensorFlow/Keras layers (or instance attributes), with layer
|
|
84
|
+
(or attribute) names as keys.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
if hasattr(self.model, "layers"):
|
|
88
|
+
layers_pool = {layer.name: layer for layer in self.model.layers}
|
|
89
|
+
if show:
|
|
90
|
+
for name, layer in layers_pool.items():
|
|
91
|
+
self._show_layer(name, layer)
|
|
92
|
+
layers_pool.update(self._get_sub_layers_pool(layers_pool, show=show))
|
|
93
|
+
else:
|
|
94
|
+
layers_pool = super()._get_layers_pool(show=show)
|
|
95
|
+
layers_pool.update(self._get_sub_layers_pool(layers_pool, show=show))
|
|
96
|
+
|
|
97
|
+
if extend_search:
|
|
98
|
+
layers_pool.update(super()._get_layers_pool(show=show))
|
|
99
|
+
|
|
100
|
+
return layers_pool
|
|
101
|
+
|
|
102
|
+
def _get_sub_layers_pool(self, layers_pool: Dict[str, tf.keras.layers.Layer | Any], show: bool = False) \
|
|
103
|
+
-> Dict[str, tf.keras.layers.Layer | Any]:
|
|
104
|
+
"""
|
|
105
|
+
Retrieves a dictionary containing all the available TensorFlow/Keras layers (or instance attributes), with the
|
|
106
|
+
layer (or attribute) names used as keys.
|
|
107
|
+
|
|
108
|
+
:param show: (optional, default is False) A boolean flag indicating whether to display the retrieved layers
|
|
109
|
+
along with their names.
|
|
110
|
+
:param layers_pool: (mandatory) A dictionary storing the model's TensorFlow/Keras layers (or instance
|
|
111
|
+
attributes), with layer (or attribute) names as keys.
|
|
112
|
+
|
|
113
|
+
:return:
|
|
114
|
+
- sub_layers_pool: A dictionary storing the model's TensorFlow/Keras layers (or instance attributes), with
|
|
115
|
+
layer (or attribute) names as keys, enhanced with sub-layers formerly encapsulated in keras.Sequential
|
|
116
|
+
objects.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
sub_layers_pool = {}
|
|
120
|
+
for name, layer in layers_pool.items():
|
|
121
|
+
if isinstance(layer, keras.Sequential):
|
|
122
|
+
sub_layers_pool.update({name + "." + sub_layer.name: sub_layer for sub_layer in layer.layers})
|
|
123
|
+
if show:
|
|
124
|
+
for name, layer in sub_layers_pool.items():
|
|
125
|
+
self._show_layer(name, layer)
|
|
126
|
+
|
|
127
|
+
return sub_layers_pool
|
|
128
|
+
|
|
129
|
+
def _show_layer(self, name: str, layer: tf.keras.layers.Layer | Any, potential: bool = False) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Displays a single available layer (or instance attribute) in the model, along with its corresponding name.
|
|
132
|
+
|
|
133
|
+
:param name: (mandatory) A string representing the name of the layer or attribute.
|
|
134
|
+
:param layer: (mandatory) A TensorFlow/Keras layer, or an instance attribute in the model.
|
|
135
|
+
:param potential: (optional, default is False) A flag indicating whether the object displayed is potentially
|
|
136
|
+
a layer (i.e., a generic instance attribute, not guaranteed to be a layer).
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
if (isinstance(layer, keras.layers.Conv1D) or isinstance(layer, keras.layers.Conv2D) or
|
|
140
|
+
isinstance(layer, keras.layers.Softmax) or isinstance(layer, keras.Sequential)):
|
|
141
|
+
super()._show_layer(name, layer, potential=potential)
|
|
142
|
+
|
|
143
|
+
def _create_raw_batched_cams(self, data_list: List[np.array], target_class: int,
|
|
144
|
+
target_layer: tf.keras.layers.Layer, explainer_type: str, softmax_final: bool) \
|
|
145
|
+
-> Tuple[List[np.ndarray], np.ndarray]:
|
|
146
|
+
"""
|
|
147
|
+
Retrieves raw CAMs from an input data list based on the specified settings (defined by algorithm, target layer,
|
|
148
|
+
and target class). Additionally, it returns the class probabilities predicted by the model.
|
|
149
|
+
|
|
150
|
+
:param data_list: (mandatory) A list of np.ndarrays to be explained, representing either a signal or an image.
|
|
151
|
+
:param target_class: (mandatory) An integer representing the target class for the explanation.
|
|
152
|
+
:param target_layer: (mandatory) A string representing the target layer for the explanation. This string should
|
|
153
|
+
identify either TensorFlow/Keras layers or it should be a class dictionary key, used to retrieve the layer
|
|
154
|
+
from the class attributes.
|
|
155
|
+
:param explainer_type: (mandatory) A string representing the desired algorithm for the explanation. This string
|
|
156
|
+
should identify one of the CAM algorithms allowed, as listed by the class constructor.
|
|
157
|
+
:param softmax_final: (mandatory) A boolean indicating whether the network terminates with a Sigmoid/Softmax
|
|
158
|
+
activation function.
|
|
159
|
+
|
|
160
|
+
:return:
|
|
161
|
+
- cam_list: A list of np.ndarray containing CAMs for each item in the input data list, corresponding to the
|
|
162
|
+
given setting (defined by algorithm, target layer, and target class).
|
|
163
|
+
- target_probs: A np.ndarray, representing the inferred class probabilities for each item in the input list.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
# Data batching
|
|
167
|
+
if not isinstance(data_list[0], tf.Tensor):
|
|
168
|
+
data_list = [tf.convert_to_tensor(x) for x in data_list]
|
|
169
|
+
if self.padding_dim is not None:
|
|
170
|
+
padded_data_list = []
|
|
171
|
+
for item in data_list:
|
|
172
|
+
pad_size = self.padding_dim - tf.shape(item)[self.time_axs]
|
|
173
|
+
if not self.time_axs:
|
|
174
|
+
zeros = tf.zeros((pad_size, tf.shape(item)[1]), dtype=item.dtype)
|
|
175
|
+
else:
|
|
176
|
+
zeros = tf.zeros((tf.shape(item)[0], pad_size), dtype=item.dtype)
|
|
177
|
+
padded_data_list.append(tf.concat([item, zeros], axis=self.time_axs))
|
|
178
|
+
data_list = padded_data_list
|
|
179
|
+
|
|
180
|
+
is_2d_layer = self._is_2d_layer(target_layer)
|
|
181
|
+
if not self.ignore_channel_dim and (is_2d_layer and len(data_list[0].shape) == 2 or not is_2d_layer
|
|
182
|
+
and len(data_list[0].shape) == 1):
|
|
183
|
+
data_list = [tf.expand_dims(x, axis=0) for x in data_list]
|
|
184
|
+
data_batch = tf.stack(data_list, axis=0)
|
|
185
|
+
|
|
186
|
+
grad_model = keras.models.Model(self.model.inputs[0], [target_layer.output, self.model.output])
|
|
187
|
+
with tf.GradientTape() as tape:
|
|
188
|
+
self.activations, outputs = grad_model(data_batch)
|
|
189
|
+
|
|
190
|
+
if softmax_final:
|
|
191
|
+
# Approximate Softmax inversion formula logit = log(prob) + constant, as the constant is negligible
|
|
192
|
+
# during derivation
|
|
193
|
+
target_scores = tf.math.log(outputs)
|
|
194
|
+
target_probs = outputs
|
|
195
|
+
else:
|
|
196
|
+
target_scores = outputs
|
|
197
|
+
target_probs = tf.nn.softmax(target_scores, axis=1)
|
|
198
|
+
|
|
199
|
+
target_scores = target_scores[:, target_class]
|
|
200
|
+
target_probs = target_probs[:, target_class]
|
|
201
|
+
self.gradients = tape.gradient(target_scores, self.activations)
|
|
202
|
+
|
|
203
|
+
cam_list = []
|
|
204
|
+
is_2d_layer = self._is_2d_layer(target_layer)
|
|
205
|
+
for i in range(len(data_list)):
|
|
206
|
+
if explainer_type == "HiResCAM":
|
|
207
|
+
cam = self._get_hirecam_map(is_2d_layer=is_2d_layer, batch_idx=i)
|
|
208
|
+
else:
|
|
209
|
+
cam = self._get_gradcam_map(is_2d_layer=is_2d_layer, batch_idx=i)
|
|
210
|
+
cam_list.append(cam.numpy())
|
|
211
|
+
|
|
212
|
+
return cam_list, target_probs
|
|
213
|
+
|
|
214
|
+
def _get_gradcam_map(self, is_2d_layer: bool, batch_idx: int) -> tf.Tensor:
|
|
215
|
+
"""
|
|
216
|
+
Compute the CAM using the vanilla Gradient-weighted Class Activation Mapping (Grad-CAM) algorithm.
|
|
217
|
+
|
|
218
|
+
:param is_2d_layer: (mandatory) A boolean indicating whether the target layers 2D-convolutional layer.
|
|
219
|
+
:param batch_idx: (mandatory) The index corresponding to the i-th selected item within the original input data
|
|
220
|
+
list.
|
|
221
|
+
|
|
222
|
+
:return: cam: A TensorFlow/Keras tensor representing the Class Activation Map (CAM) for the batch_idx-th input,
|
|
223
|
+
built with the Grad-CAM algorithm.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
if is_2d_layer:
|
|
227
|
+
dim_mean = (0, 1)
|
|
228
|
+
else:
|
|
229
|
+
dim_mean = 0
|
|
230
|
+
weights = tf.reduce_mean(self.gradients[batch_idx], axis=dim_mean)
|
|
231
|
+
activations = self.activations[batch_idx].numpy()
|
|
232
|
+
|
|
233
|
+
for i in range(activations.shape[-1]):
|
|
234
|
+
if is_2d_layer:
|
|
235
|
+
activations[:, :, i] *= weights[i]
|
|
236
|
+
else:
|
|
237
|
+
activations[:, i] *= weights[i]
|
|
238
|
+
activations[:, i] *= weights[i]
|
|
239
|
+
|
|
240
|
+
cam = tf.reduce_sum(tf.convert_to_tensor(activations), axis=-1)
|
|
241
|
+
cam = tf.nn.relu(cam)
|
|
242
|
+
return cam
|
|
243
|
+
|
|
244
|
+
def _get_hirecam_map(self, is_2d_layer: bool, batch_idx: int) -> tf.Tensor:
|
|
245
|
+
"""
|
|
246
|
+
Compute the CAM using the High-Resolution Class Activation Mapping (HiResCAM) algorithm.
|
|
247
|
+
|
|
248
|
+
:param is_2d_layer: (mandatory) A boolean indicating whether the target layers 2D-convolutional layer.
|
|
249
|
+
:param batch_idx: (mandatory) The index corresponding to the i-th selected item within the original input data
|
|
250
|
+
list.
|
|
251
|
+
|
|
252
|
+
:return: cam: A TensorFlow/Keras tensor representing the Class Activation Map (CAM) for the batch_idx-th input,
|
|
253
|
+
built with the HiResCAM algorithm.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
activations = self.activations[batch_idx].numpy()
|
|
257
|
+
gradients = self.gradients[batch_idx].numpy()
|
|
258
|
+
|
|
259
|
+
for i in range(activations.shape[-1]):
|
|
260
|
+
if is_2d_layer:
|
|
261
|
+
activations[:, :, i] *= gradients[:, :, i]
|
|
262
|
+
else:
|
|
263
|
+
activations[:, i] *= gradients[:, i]
|
|
264
|
+
|
|
265
|
+
cam = tf.reduce_sum(tf.convert_to_tensor(activations), axis=-1)
|
|
266
|
+
cam = tf.nn.relu(cam)
|
|
267
|
+
return cam
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
def _is_2d_layer(target_layer: tf.keras.layers.Layer) -> bool:
|
|
271
|
+
"""
|
|
272
|
+
Evaluates whether the target layer is a 2D-convolutional layer.
|
|
273
|
+
|
|
274
|
+
:param target_layer: (mandatory) A TensorFlow/Keras layer.
|
|
275
|
+
|
|
276
|
+
:return:
|
|
277
|
+
- is_2d_layer: A boolean indicating whether the target layers 2D-convolutional layer.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
if isinstance(target_layer, keras.layers.Conv1D):
|
|
281
|
+
is_2d_layer = False
|
|
282
|
+
elif isinstance(target_layer, keras.layers.Conv2D):
|
|
283
|
+
is_2d_layer = True
|
|
284
|
+
else:
|
|
285
|
+
is_2d_layer = CamBuilder._is_2d_layer(target_layer)
|
|
286
|
+
return is_2d_layer
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Samuele Pe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|