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.

@@ -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.