From f15d7a8f2688c14fdb362f503c3b050c7363d97f Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Wed, 30 Nov 2022 14:08:26 +0000 Subject: [PATCH 01/35] Filter gradients of IndexedSlices in tf grad sim backend, fixes #828 --- .../similarity/backends/tensorflow/base.py | 4 +++- .../tests/test_simiarlity/test_backends.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index 0b6e2492e..9ade99212 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -50,7 +50,9 @@ def get_grads( # compute gradients of the loss w.r.t the weights grad_X_train = tape.gradient(loss, model.trainable_weights) - grad_X_train = np.concatenate([w.numpy().reshape(-1) for w in grad_X_train]) + # see https://github.com/SeldonIO/alibi/issues/828 w.r.t. filtering out tf.IndexedSlices + grad_X_train = np.concatenate([w.numpy().reshape(-1) for w in grad_X_train + if not isinstance(w, tf.IndexedSlices)]) return grad_X_train @staticmethod diff --git a/alibi/explainers/tests/test_simiarlity/test_backends.py b/alibi/explainers/tests/test_simiarlity/test_backends.py index 6ca79dcb8..e250fa1b0 100644 --- a/alibi/explainers/tests/test_simiarlity/test_backends.py +++ b/alibi/explainers/tests/test_simiarlity/test_backends.py @@ -2,6 +2,7 @@ import torch import numpy as np +import tensorflow as tf from alibi.explainers.similarity.backends.tensorflow.base import _TensorFlowBackend from alibi.explainers.similarity.backends.pytorch.base import _PytorchBackend @@ -41,3 +42,21 @@ def test_backends(random_cls_dataset, linear_models): torch_grads = np.sort(torch_grads) tf_grads = np.sort(tf_grads) np.testing.assert_allclose(torch_grads, tf_grads, rtol=1e-04) + + +def test_tf_embedding_similarity(): + """Test that the `tensorflow` embedding similarity backend works as expected. + + See https://github.com/SeldonIO/alibi/issues/828. + """ + model = tf.keras.models.Sequential([ + tf.keras.layers.Embedding(10, 4, input_shape=(5,)), + tf.keras.layers.Flatten(), + tf.keras.layers.Dense(1) + ]) + + X = tf.random.uniform(shape=(1, 5), minval=0, maxval=10, dtype=tf.float32) + Y = tf.random.uniform(shape=(1, 1), minval=0, maxval=10, dtype=tf.float32) + loss_fn = tf.keras.losses.MeanSquaredError() + tf_grads = _TensorFlowBackend.get_grads(model, X, Y, loss_fn) + assert tf_grads.shape == (21, ) # (5 * 4) + 1 From 1fd9c384a69323b552b9b962c85ed618b8f12060 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Thu, 5 Jan 2023 16:43:57 +0000 Subject: [PATCH 02/35] Add functionality to convert sparse gradients to nd.array --- .../similarity/backends/pytorch/base.py | 14 ++++- .../similarity/backends/tensorflow/base.py | 10 +++- .../tests/test_simiarlity/test_backends.py | 30 +++++++++- .../test_grad_methods_integration.py | 56 +++++++++++++++++++ 4 files changed, 103 insertions(+), 7 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index a000312f5..134f249f3 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -50,8 +50,18 @@ def get_grads( loss = loss_fn(output, Y) loss.backward() model.train(initial_model_state) - return np.concatenate([_PytorchBackend.to_numpy(param.grad).reshape(-1) # type: ignore [arg-type] # see #810 - for param in model.parameters()]) + + return np.concatenate([_PytorchBackend._grad_to_numpy(param.grad) # type: ignore [arg-type] # see #810 + for param in model.parameters() + if param.grad is not None]) + + @staticmethod + def _grad_to_numpy(grad: torch.Tensor) -> torch.Tensor: + """Flattens a gradient tensor.""" + if grad.is_sparse: + grad = grad.to_dense() + grad = grad.reshape(-1).detach().cpu() + return grad.numpy() @staticmethod def to_tensor(X: np.ndarray) -> torch.Tensor: diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index 9ade99212..e1efdc6bf 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -51,10 +51,16 @@ def get_grads( # compute gradients of the loss w.r.t the weights grad_X_train = tape.gradient(loss, model.trainable_weights) # see https://github.com/SeldonIO/alibi/issues/828 w.r.t. filtering out tf.IndexedSlices - grad_X_train = np.concatenate([w.numpy().reshape(-1) for w in grad_X_train - if not isinstance(w, tf.IndexedSlices)]) + grad_X_train = np.concatenate([_TensorFlowBackend._grad_to_numpy(w) for w in grad_X_train]) return grad_X_train + @staticmethod + def _grad_to_numpy(grad: tf.Tensor) -> tf.Tensor: + """Flattens a gradient tensor.""" + if isinstance(grad, tf.IndexedSlices): + grad = tf.convert_to_tensor(grad) + return grad.numpy().reshape(-1) + @staticmethod def to_tensor(X: np.ndarray) -> tf.Tensor: """Converts a `numpy` array to a `tensorflow` tensor.""" diff --git a/alibi/explainers/tests/test_simiarlity/test_backends.py b/alibi/explainers/tests/test_simiarlity/test_backends.py index e250fa1b0..730132c3d 100644 --- a/alibi/explainers/tests/test_simiarlity/test_backends.py +++ b/alibi/explainers/tests/test_simiarlity/test_backends.py @@ -44,13 +44,14 @@ def test_backends(random_cls_dataset, linear_models): np.testing.assert_allclose(torch_grads, tf_grads, rtol=1e-04) -def test_tf_embedding_similarity(): +@pytest.mark.parametrize('trainable_emd, grads_shape', [(True, (61, )), (False, (21, ))]) +def test_tf_embedding_similarity(trainable_emd, grads_shape): """Test that the `tensorflow` embedding similarity backend works as expected. See https://github.com/SeldonIO/alibi/issues/828. """ model = tf.keras.models.Sequential([ - tf.keras.layers.Embedding(10, 4, input_shape=(5,)), + tf.keras.layers.Embedding(10, 4, input_shape=(5,), trainable=trainable_emd), tf.keras.layers.Flatten(), tf.keras.layers.Dense(1) ]) @@ -59,4 +60,27 @@ def test_tf_embedding_similarity(): Y = tf.random.uniform(shape=(1, 1), minval=0, maxval=10, dtype=tf.float32) loss_fn = tf.keras.losses.MeanSquaredError() tf_grads = _TensorFlowBackend.get_grads(model, X, Y, loss_fn) - assert tf_grads.shape == (21, ) # (5 * 4) + 1 + assert tf_grads.shape == grads_shape # (4 * 10) + (5 * 4) + 1 + + +@pytest.mark.parametrize('trainable_emd, grads_shape', [(True, (61, )), (False, (21, ))]) +@pytest.mark.parametrize('sparse', [True, False]) +def test_pytorch_embedding_similarity(trainable_emd, grads_shape, sparse): + """Test that the `pytorch` embedding similarity backend works as expected.""" + + model = torch.nn.Sequential( + torch.nn.Embedding(10, 4, 5, sparse=sparse), + torch.nn.Flatten(), + torch.nn.LazyLinear(1) + ) + + if trainable_emd: + model[0].weight.requires_grad = True + else: + model[0].weight.requires_grad = False + + X = torch.randint(0, 10, (1, 5)) + Y = torch.randint(0, 10, (1, 1), dtype=torch.float32) + loss_fn = torch.nn.MSELoss() + tf_grads = _PytorchBackend.get_grads(model, X, Y, loss_fn) + assert tf_grads.shape == grads_shape # (4 * 10) + (5 * 4) + 1 diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index bbb80c9f2..c8a98971c 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -14,7 +14,9 @@ import pytest import numpy as np +import torch import torch.nn as nn +import tensorflow as tf from tensorflow import keras from alibi.explainers.similarity.grad import GradientSimilarity @@ -303,3 +305,57 @@ def test_multiple_test_instances_stored_grads_asym_dot(precompute_grads): scores = np.array([[50, 50, 2*50**2]], dtype=np.float32) / (denoms + 1e-7) scores.sort() np.testing.assert_allclose(explanation.scores, scores[:, ::-1], atol=1e-4) + + +def test_non_trainable_layers_warning_tf(): + """ + Test that users are warned when passing models with non-trainable layers. + """ + model = tf.keras.models.Sequential([ + tf.keras.layers.Embedding(10, 4, input_shape=(5,), trainable=True), + tf.keras.layers.Flatten(), + tf.keras.layers.Dense(1) + ]) + + X = tf.random.uniform(shape=(1, 5), minval=0, maxval=10, dtype=tf.float32) + Y = tf.random.uniform(shape=(1, 1), minval=0, maxval=10, dtype=tf.float32) + loss_fn = tf.keras.losses.MeanSquaredError() + model.layers[1].trainable = False + + with pytest.warns(UserWarning) as warning: + GradientSimilarity( + model, + task='regression', + loss_fn=loss_fn, + sim_fn='grad_asym_dot', + backend='tensorflow', + precompute_grads=False + ) + print(warning) + + +def test_non_trainable_layers_warning_torch(): + """ + Test that users are warned when passing models with non-trainable layers. + """ + model = torch.nn.Sequential( + torch.nn.Embedding(10, 4, 5, sparse=True), + torch.nn.Flatten(), + torch.nn.LazyLinear(1) + ) + + X = torch.randint(0, 10, (1, 5)) + Y = torch.randint(0, 10, (1, 1), dtype=torch.float32) + loss_fn = torch.nn.MSELoss() + model[2].weight.requires_grad = False + + with pytest.warns(UserWarning) as warning: + GradientSimilarity( + model, + task='regression', + loss_fn=loss_fn, + sim_fn='grad_asym_dot', + backend='tensorflow', + precompute_grads=False + ) + print(warning) \ No newline at end of file From 3dbc6c6b31d895d077d8b99bcdc03017bcb851d6 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Thu, 5 Jan 2023 17:37:54 +0000 Subject: [PATCH 03/35] Add warnings if non-trainable layers present in model --- .../similarity/backends/pytorch/base.py | 9 ++++++++ .../similarity/backends/tensorflow/base.py | 7 ++++++ alibi/explainers/similarity/grad.py | 5 +++++ .../tests/test_simiarlity/test_backends.py | 5 +---- .../test_grad_methods_integration.py | 22 +++++++++---------- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index 134f249f3..bcad70679 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -94,3 +94,12 @@ def to_numpy(X: torch.Tensor) -> np.ndarray: def argmax(X: torch.Tensor, dim=-1) -> torch.Tensor: """Returns the index of the maximum value in a tensor.""" return torch.argmax(X, dim=dim) + + @staticmethod + def check_all_layers_trainable(model: nn.Module) -> None: + """Checks that all layers in a model are trainable.""" + for param in model.parameters(): + if not param.requires_grad: + return False + return True + \ No newline at end of file diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index e1efdc6bf..20bbdb064 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -87,3 +87,10 @@ def argmax(X: tf.Tensor, dim=-1) -> tf.Tensor: """Returns the index of the maximum value in a tensor.""" X = tf.math.argmax(X, axis=dim) return X + + @staticmethod + def check_all_layers_trainable(model: keras.Model) -> None: + """Checks if all layers in a model are trainable.""" + if len(model.weights) != len(model.trainable_weights): + return False + return True diff --git a/alibi/explainers/similarity/grad.py b/alibi/explainers/similarity/grad.py index e4b8b7c5c..c14ceefc8 100644 --- a/alibi/explainers/similarity/grad.py +++ b/alibi/explainers/similarity/grad.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Callable, Optional, Union, Dict, Tuple from typing_extensions import Literal from enum import Enum +import warnings import numpy as np @@ -128,6 +129,10 @@ def __init__(self, task_name=task ) + if not self.backend.check_all_layers_trainable(self.predictor): + warnings.warn(("Some layers in the model are not trainable. These layer gradients will not be " + "included in the computation of gradient similarity.")) + def fit(self, X_train: np.ndarray, Y_train: np.ndarray) -> "Explainer": diff --git a/alibi/explainers/tests/test_simiarlity/test_backends.py b/alibi/explainers/tests/test_simiarlity/test_backends.py index 730132c3d..7b368b9bd 100644 --- a/alibi/explainers/tests/test_simiarlity/test_backends.py +++ b/alibi/explainers/tests/test_simiarlity/test_backends.py @@ -74,10 +74,7 @@ def test_pytorch_embedding_similarity(trainable_emd, grads_shape, sparse): torch.nn.LazyLinear(1) ) - if trainable_emd: - model[0].weight.requires_grad = True - else: - model[0].weight.requires_grad = False + model[0].weight.requires_grad = trainable_emd X = torch.randint(0, 10, (1, 5)) Y = torch.randint(0, 10, (1, 1), dtype=torch.float32) diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index c8a98971c..0d39e1bbe 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -317,12 +317,10 @@ def test_non_trainable_layers_warning_tf(): tf.keras.layers.Dense(1) ]) - X = tf.random.uniform(shape=(1, 5), minval=0, maxval=10, dtype=tf.float32) - Y = tf.random.uniform(shape=(1, 1), minval=0, maxval=10, dtype=tf.float32) loss_fn = tf.keras.losses.MeanSquaredError() - model.layers[1].trainable = False + model.layers[2].trainable = False - with pytest.warns(UserWarning) as warning: + with pytest.warns(Warning) as record: GradientSimilarity( model, task='regression', @@ -331,7 +329,9 @@ def test_non_trainable_layers_warning_tf(): backend='tensorflow', precompute_grads=False ) - print(warning) + + assert str(record[0].message) == ("Some layers in the model are not trainable. These layer gradients will not be " + "included in the computation of gradient similarity.") def test_non_trainable_layers_warning_torch(): @@ -339,23 +339,23 @@ def test_non_trainable_layers_warning_torch(): Test that users are warned when passing models with non-trainable layers. """ model = torch.nn.Sequential( - torch.nn.Embedding(10, 4, 5, sparse=True), + torch.nn.Embedding(10, 4, 5), torch.nn.Flatten(), torch.nn.LazyLinear(1) ) - X = torch.randint(0, 10, (1, 5)) - Y = torch.randint(0, 10, (1, 1), dtype=torch.float32) loss_fn = torch.nn.MSELoss() model[2].weight.requires_grad = False - with pytest.warns(UserWarning) as warning: + with pytest.warns(Warning) as record: GradientSimilarity( model, task='regression', loss_fn=loss_fn, sim_fn='grad_asym_dot', - backend='tensorflow', + backend='pytorch', precompute_grads=False ) - print(warning) \ No newline at end of file + + assert str(record[0].message) == ("Some layers in the model are not trainable. These layer gradients will not be " + "included in the computation of gradient similarity.") From ab16515013c492236258b9a03720ec3a94c74ddc Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Thu, 5 Jan 2023 17:44:14 +0000 Subject: [PATCH 04/35] Fix linting and typing errors --- alibi/explainers/similarity/backends/pytorch/base.py | 3 +-- alibi/explainers/similarity/backends/tensorflow/base.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index bcad70679..3c77353de 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -96,10 +96,9 @@ def argmax(X: torch.Tensor, dim=-1) -> torch.Tensor: return torch.argmax(X, dim=dim) @staticmethod - def check_all_layers_trainable(model: nn.Module) -> None: + def check_all_layers_trainable(model: nn.Module) -> bool: """Checks that all layers in a model are trainable.""" for param in model.parameters(): if not param.requires_grad: return False return True - \ No newline at end of file diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index 20bbdb064..7e1eb246a 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -89,7 +89,7 @@ def argmax(X: tf.Tensor, dim=-1) -> tf.Tensor: return X @staticmethod - def check_all_layers_trainable(model: keras.Model) -> None: + def check_all_layers_trainable(model: keras.Model) -> bool: """Checks if all layers in a model are trainable.""" if len(model.weights) != len(model.trainable_weights): return False From 87d4ca7d291645dc15ae6b8fe7faa24a1f20e82c Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Fri, 6 Jan 2023 09:54:59 +0000 Subject: [PATCH 05/35] Add check for non-trainable layers in gradsim --- .../similarity/backends/tensorflow/base.py | 4 +- .../tests/test_simiarlity/conftest.py | 25 ++++--- .../test_grad_methods_integration.py | 71 ++++++++++--------- 3 files changed, 56 insertions(+), 44 deletions(-) diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index 7e1eb246a..4f554ac71 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -91,6 +91,8 @@ def argmax(X: tf.Tensor, dim=-1) -> tf.Tensor: @staticmethod def check_all_layers_trainable(model: keras.Model) -> bool: """Checks if all layers in a model are trainable.""" - if len(model.weights) != len(model.trainable_weights): + for weight in model.non_trainable_weights: + if weight.name.startswith('batch_normalization'): + continue return False return True diff --git a/alibi/explainers/tests/test_simiarlity/conftest.py b/alibi/explainers/tests/test_simiarlity/conftest.py index 9976ad594..547391783 100644 --- a/alibi/explainers/tests/test_simiarlity/conftest.py +++ b/alibi/explainers/tests/test_simiarlity/conftest.py @@ -82,10 +82,11 @@ def linear_cls_model(request): input_shape = request.param.get('input_shape', (10,)) output_shape = request.param.get('output_shape', 10) framework = request.param.get('framework', 'tensorflow') + batch_norm = request.param.get('batch_norm', False) model = { - 'tensorflow': lambda i_shape, o_shape: tf_linear_model(i_shape, o_shape), - 'pytorch': lambda i_shape, o_shape: torch_linear_model(i_shape, o_shape) + 'tensorflow': lambda i_shape, o_shape: tf_linear_model(i_shape, o_shape, batch_norm), + 'pytorch': lambda i_shape, o_shape: torch_linear_model(i_shape, o_shape, batch_norm) }[framework](input_shape, output_shape) loss_fn = { @@ -143,18 +144,25 @@ def linear_models(request): return tf_model, tf_loss, torch_model, torch_loss -def tf_linear_model(input_shape, output_shape): +def tf_linear_model(input_shape, output_shape, batch_norm=False): """ Constructs a linear model for `tensorflow`. """ - return keras.Sequential([ - keras.layers.InputLayer(input_shape=input_shape), - keras.layers.Dense(output_shape), - keras.layers.Softmax() + layers = [ + tf.keras.layers.InputLayer(input_shape=input_shape), + tf.keras.layers.Flatten(), + ] + if batch_norm: + layers.append(tf.keras.layers.BatchNormalization()) + layers.extend([ + tf.keras.layers.Dense(output_shape), + tf.keras.layers.Softmax() ]) + return keras.Sequential(layers) + -def torch_linear_model(input_shape_arg, output_shape_arg): +def torch_linear_model(input_shape_arg, output_shape_arg, batch_norm=False): """ Constructs a linear model for `torch`. """ @@ -165,6 +173,7 @@ def __init__(self, input_shape, output_shape): super(Model, self).__init__() self.linear_stack = nn.Sequential( nn.Flatten(start_dim=1), + nn.BatchNorm1d(input_shape) if batch_norm else nn.Identity(), nn.Linear(input_shape, output_shape), nn.Softmax() ) diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index 0d39e1bbe..600f7b744 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -12,11 +12,10 @@ """ import pytest +import warnings import numpy as np -import torch import torch.nn as nn -import tensorflow as tf from tensorflow import keras from alibi.explainers.similarity.grad import GradientSimilarity @@ -140,6 +139,7 @@ def test_correct_grad_dot_sim_result_tf(seed, normed_ds): `tensorflow` backend. """ model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + model.build((None, 2)) explainer = GradientSimilarity( model, task='regression', @@ -162,6 +162,7 @@ def test_correct_grad_cos_sim_result_tf(seed, ds): `tensorflow` backend. """ model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + model.build((None, 2)) explainer = GradientSimilarity( model, task='regression', @@ -184,6 +185,7 @@ def test_grad_dot_result_order_tf(seed): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + model.build((None, 2)) explainer = GradientSimilarity( model, task='regression', @@ -204,6 +206,7 @@ def test_grad_cos_result_order_tf(seed): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + model.build((None, 2)) explainer = GradientSimilarity( model, task='regression', @@ -224,6 +227,7 @@ def test_multiple_test_instances_grad_cos(precompute_grads): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + model.build((None, 2)) explainer = GradientSimilarity( model, task='regression', @@ -250,6 +254,7 @@ def test_multiple_test_instances_grad_dot(precompute_grads): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + model.build((None, 2)) explainer = GradientSimilarity( model, task='regression', @@ -273,6 +278,7 @@ def test_multiple_test_instances_stored_grads_asym_dot(precompute_grads): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + model.build((None, 2)) explainer = GradientSimilarity( model, task='regression', @@ -307,55 +313,50 @@ def test_multiple_test_instances_stored_grads_asym_dot(precompute_grads): np.testing.assert_allclose(explanation.scores, scores[:, ::-1], atol=1e-4) -def test_non_trainable_layers_warning_tf(): +@pytest.mark.parametrize('linear_cls_model', + [ + ({'framework': 'pytorch', 'input_shape': (10,), 'output_shape': 10, 'batch_norm': True}), + ({'framework': 'tensorflow', 'input_shape': (10,), 'output_shape': 10, 'batch_norm': True}) + ], + indirect=True, ids=['torch-model', 'tf-model']) +def test_no_warnings_batch_norm(linear_cls_model): """ - Test that users are warned when passing models with non-trainable layers. + Test that no warnings are raised when using batch norm layers. """ - model = tf.keras.models.Sequential([ - tf.keras.layers.Embedding(10, 4, input_shape=(5,), trainable=True), - tf.keras.layers.Flatten(), - tf.keras.layers.Dense(1) - ]) - - loss_fn = tf.keras.losses.MeanSquaredError() - model.layers[2].trainable = False - - with pytest.warns(Warning) as record: + backend, model, loss_fn, target_fn = linear_cls_model + with warnings.catch_warnings(): + warnings.simplefilter("error") GradientSimilarity( model, - task='regression', + task='classification', loss_fn=loss_fn, - sim_fn='grad_asym_dot', - backend='tensorflow', - precompute_grads=False + backend=backend, ) - assert str(record[0].message) == ("Some layers in the model are not trainable. These layer gradients will not be " - "included in the computation of gradient similarity.") - -def test_non_trainable_layers_warning_torch(): +@pytest.mark.parametrize('linear_cls_model', + [ + ({'framework': 'pytorch', 'input_shape': (10,), 'output_shape': 10}), + ({'framework': 'tensorflow', 'input_shape': (10,), 'output_shape': 10}) + ], + indirect=True, ids=['torch-model', 'tf-model']) +def test_non_trainable_layer_warnings(linear_cls_model): """ - Test that users are warned when passing models with non-trainable layers. + Test that no warnings are raised when using batch norm layers. """ - model = torch.nn.Sequential( - torch.nn.Embedding(10, 4, 5), - torch.nn.Flatten(), - torch.nn.LazyLinear(1) - ) + backend, model, loss_fn, target_fn = linear_cls_model - loss_fn = torch.nn.MSELoss() - model[2].weight.requires_grad = False + if backend == 'pytorch': + model.linear_stack[2].weight.requires_grad = False + elif backend == 'tensorflow': + model.layers[1].trainable = False with pytest.warns(Warning) as record: GradientSimilarity( model, - task='regression', + task='classification', loss_fn=loss_fn, - sim_fn='grad_asym_dot', - backend='pytorch', - precompute_grads=False + backend=backend, ) - assert str(record[0].message) == ("Some layers in the model are not trainable. These layer gradients will not be " "included in the computation of gradient similarity.") From 6cad12cabb9a47a4e460621fe19e7edf5922698b Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Fri, 6 Jan 2023 10:18:33 +0000 Subject: [PATCH 06/35] Add better docstrings --- .../explainers/similarity/backends/pytorch/base.py | 6 +++++- .../similarity/backends/tensorflow/base.py | 14 +++++++++++--- .../test_grad_methods_integration.py | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index 3c77353de..88fbe2d40 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -57,7 +57,11 @@ def get_grads( @staticmethod def _grad_to_numpy(grad: torch.Tensor) -> torch.Tensor: - """Flattens a gradient tensor.""" + """Convert graidient to numpy array. + + Converts gradient tensor to flat numpy array. If the gradient is a sparse tensor, it is converted to a dense \ + tensor first. + """ if grad.is_sparse: grad = grad.to_dense() grad = grad.reshape(-1).detach().cpu() diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index 4f554ac71..b08a3f348 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -50,14 +50,19 @@ def get_grads( # compute gradients of the loss w.r.t the weights grad_X_train = tape.gradient(loss, model.trainable_weights) - # see https://github.com/SeldonIO/alibi/issues/828 w.r.t. filtering out tf.IndexedSlices grad_X_train = np.concatenate([_TensorFlowBackend._grad_to_numpy(w) for w in grad_X_train]) return grad_X_train @staticmethod def _grad_to_numpy(grad: tf.Tensor) -> tf.Tensor: - """Flattens a gradient tensor.""" + """Convert graidient to numpy array. + + Converts gradient tensor to flat numpy array. If the gradient is a sparse tensor, it is converted to a dense \ + tensor first. + """ + if isinstance(grad, tf.IndexedSlices): + # see https://github.com/SeldonIO/alibi/issues/828 grad = tf.convert_to_tensor(grad) return grad.numpy().reshape(-1) @@ -90,7 +95,10 @@ def argmax(X: tf.Tensor, dim=-1) -> tf.Tensor: @staticmethod def check_all_layers_trainable(model: keras.Model) -> bool: - """Checks if all layers in a model are trainable.""" + """Checks if all layers in a model are trainable. + + Note: batch normalization layers are ignored as they are not trainable by default. + """ for weight in model.non_trainable_weights: if weight.name.startswith('batch_normalization'): continue diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index 600f7b744..fafa622db 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -342,7 +342,7 @@ def test_no_warnings_batch_norm(linear_cls_model): indirect=True, ids=['torch-model', 'tf-model']) def test_non_trainable_layer_warnings(linear_cls_model): """ - Test that no warnings are raised when using batch norm layers. + Test that warnings are raised when user passes non-trainbable layers to grad sim method. """ backend, model, loss_fn, target_fn = linear_cls_model From 12937d84a4d52a736f10ba491ac83df19f01034d Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Fri, 6 Jan 2023 10:25:37 +0000 Subject: [PATCH 07/35] Add better docstrings for tests --- .../explainers/tests/test_simiarlity/test_backends.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/alibi/explainers/tests/test_simiarlity/test_backends.py b/alibi/explainers/tests/test_simiarlity/test_backends.py index 7b368b9bd..aff680cb8 100644 --- a/alibi/explainers/tests/test_simiarlity/test_backends.py +++ b/alibi/explainers/tests/test_simiarlity/test_backends.py @@ -46,8 +46,10 @@ def test_backends(random_cls_dataset, linear_models): @pytest.mark.parametrize('trainable_emd, grads_shape', [(True, (61, )), (False, (21, ))]) def test_tf_embedding_similarity(trainable_emd, grads_shape): - """Test that the `tensorflow` embedding similarity backend works as expected. + """Test GradSim explainer correctly handles sparcity and non-trainable layers for tensorflow. + Test that `tensorflow` embedding layers work as expected and also that layers + marked as non-trainable are not included in the gradients. See https://github.com/SeldonIO/alibi/issues/828. """ model = tf.keras.models.Sequential([ @@ -66,7 +68,11 @@ def test_tf_embedding_similarity(trainable_emd, grads_shape): @pytest.mark.parametrize('trainable_emd, grads_shape', [(True, (61, )), (False, (21, ))]) @pytest.mark.parametrize('sparse', [True, False]) def test_pytorch_embedding_similarity(trainable_emd, grads_shape, sparse): - """Test that the `pytorch` embedding similarity backend works as expected.""" + """Test GradSim explainer correctly handles sparcity and non-trainable layers for pytorch. + + Tests that the `pytorch` embedding layers work as expected and that layers marked as + non-trainable are not included in the gradients. + """ model = torch.nn.Sequential( torch.nn.Embedding(10, 4, 5, sparse=sparse), From 219a6a0e8c9ef2cfdfb0d93274d3b66ec6c55d7f Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Fri, 6 Jan 2023 10:46:05 +0000 Subject: [PATCH 08/35] Fix minor linting error --- alibi/explainers/tests/test_simiarlity/test_backends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alibi/explainers/tests/test_simiarlity/test_backends.py b/alibi/explainers/tests/test_simiarlity/test_backends.py index aff680cb8..75cdd3d51 100644 --- a/alibi/explainers/tests/test_simiarlity/test_backends.py +++ b/alibi/explainers/tests/test_simiarlity/test_backends.py @@ -70,7 +70,7 @@ def test_tf_embedding_similarity(trainable_emd, grads_shape): def test_pytorch_embedding_similarity(trainable_emd, grads_shape, sparse): """Test GradSim explainer correctly handles sparcity and non-trainable layers for pytorch. - Tests that the `pytorch` embedding layers work as expected and that layers marked as + Tests that the `pytorch` embedding layers work as expected and that layers marked as non-trainable are not included in the gradients. """ From 764bc56bf5944b8c544de21dab898e0027a561c9 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Fri, 6 Jan 2023 14:27:20 +0000 Subject: [PATCH 09/35] Add check for tensor attribute --- .../similarity/backends/pytorch/base.py | 14 ++++--- .../similarity/backends/tensorflow/base.py | 10 ++++- .../tests/test_simiarlity/test_backends.py | 42 +++++++++++++++++++ 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index 88fbe2d40..7ee8690d6 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -51,12 +51,12 @@ def get_grads( loss.backward() model.train(initial_model_state) - return np.concatenate([_PytorchBackend._grad_to_numpy(param.grad) # type: ignore [arg-type] # see #810 - for param in model.parameters() + return np.concatenate([_PytorchBackend._grad_to_numpy(grad=param.grad, name=name) + for name, param in model.named_parameters() if param.grad is not None]) @staticmethod - def _grad_to_numpy(grad: torch.Tensor) -> torch.Tensor: + def _grad_to_numpy(grad: torch.Tensor, name: Optional[str] = None) -> torch.Tensor: """Convert graidient to numpy array. Converts gradient tensor to flat numpy array. If the gradient is a sparse tensor, it is converted to a dense \ @@ -64,8 +64,12 @@ def _grad_to_numpy(grad: torch.Tensor) -> torch.Tensor: """ if grad.is_sparse: grad = grad.to_dense() - grad = grad.reshape(-1).detach().cpu() - return grad.numpy() + + if not hasattr(grad, 'numpy'): + name = f' for the named tensor: {name}' if name else '' + raise TypeError((f'Could not convert gradient to numpy array{name}. To ignore these ' + 'gradients in the similarity computation use `requires_grad=False`.')) + return grad.reshape(-1).cpu().numpy() @staticmethod def to_tensor(X: np.ndarray) -> torch.Tensor: diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index b08a3f348..b2a1430ba 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -50,11 +50,12 @@ def get_grads( # compute gradients of the loss w.r.t the weights grad_X_train = tape.gradient(loss, model.trainable_weights) - grad_X_train = np.concatenate([_TensorFlowBackend._grad_to_numpy(w) for w in grad_X_train]) + grad_X_train = np.concatenate([_TensorFlowBackend._grad_to_numpy(w, getattr(w, 'name', None)) + for w in grad_X_train]) return grad_X_train @staticmethod - def _grad_to_numpy(grad: tf.Tensor) -> tf.Tensor: + def _grad_to_numpy(grad: tf.Tensor, name: Optional[str] = None) -> tf.Tensor: """Convert graidient to numpy array. Converts gradient tensor to flat numpy array. If the gradient is a sparse tensor, it is converted to a dense \ @@ -64,6 +65,11 @@ def _grad_to_numpy(grad: tf.Tensor) -> tf.Tensor: if isinstance(grad, tf.IndexedSlices): # see https://github.com/SeldonIO/alibi/issues/828 grad = tf.convert_to_tensor(grad) + + if not hasattr(grad, 'numpy'): + name = f' for the named tensor: {name}' if name else '' + raise TypeError((f'Could not convert gradient to numpy array{name}. To ignore these ' + 'gradients in the similarity computation use `trainable=False`.')) return grad.numpy().reshape(-1) @staticmethod diff --git a/alibi/explainers/tests/test_simiarlity/test_backends.py b/alibi/explainers/tests/test_simiarlity/test_backends.py index 75cdd3d51..72a401654 100644 --- a/alibi/explainers/tests/test_simiarlity/test_backends.py +++ b/alibi/explainers/tests/test_simiarlity/test_backends.py @@ -87,3 +87,45 @@ def test_pytorch_embedding_similarity(trainable_emd, grads_shape, sparse): loss_fn = torch.nn.MSELoss() tf_grads = _PytorchBackend.get_grads(model, X, Y, loss_fn) assert tf_grads.shape == grads_shape # (4 * 10) + (5 * 4) + 1 + + +def test_non_numpy_grads_pytorch(): + """Test that the `pytorch` backend handles non-numpy gradients correctly. + + _PytorchBackend should throw an error if the gradients cannot be converted to numpy arrays. + """ + class MockTensor(): + is_sparse = False + + with pytest.raises(TypeError) as err: + _PytorchBackend._grad_to_numpy(MockTensor()) + + assert ("Could not convert gradient to numpy array. To ignore these gradients in" + " the similarity computation use `requires_grad=False`.") in str(err.value) + + with pytest.raises(TypeError) as err: + _PytorchBackend._grad_to_numpy(MockTensor(), 'test') + + assert ("Could not convert gradient to numpy array for the named tensor: test. " + "To ignore these gradients in the similarity computation use `requires_grad=False`.") in str(err.value) + + +def test_non_numpy_grads_tensorflow(): + """Test that the `tensorflow` backend handles non-numpy gradients correctly. + + _TensorFlowBackend should throw an error if the gradients cannot be converted to numpy arrays. + """ + class MockTensor(): + is_sparse = False + + with pytest.raises(TypeError) as err: + _TensorFlowBackend._grad_to_numpy(MockTensor()) + + assert ("Could not convert gradient to numpy array. To ignore these gradients " + "in the similarity computation use `trainable=False`.") in str(err.value) + + with pytest.raises(TypeError) as err: + _TensorFlowBackend._grad_to_numpy(MockTensor(), 'test') + + assert ("Could not convert gradient to numpy array for the named tensor: test." + " To ignore these gradients in the similarity computation use `trainable=False`.") in str(err.value) From 4a89f4823e35c3ceb12196c6d08f5acd702e2671 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Fri, 6 Jan 2023 14:55:43 +0000 Subject: [PATCH 10/35] Check for and log list of non trainable layers on GradSim __init__ --- .../explainers/similarity/backends/pytorch/base.py | 10 ++++------ .../similarity/backends/tensorflow/base.py | 10 +++------- alibi/explainers/similarity/grad.py | 10 +++++++--- alibi/explainers/tests/test_simiarlity/conftest.py | 2 ++ .../test_grad_methods_integration.py | 14 ++++++++++++-- .../test_simiarlity/test_grad_methods_unit.py | 2 +- 6 files changed, 29 insertions(+), 19 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index 7ee8690d6..de6d5466d 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -4,7 +4,7 @@ backend in order to ensure that the similarity methods only require to match this interface. """ -from typing import Callable, Union, Optional +from typing import Callable, Union, Optional, List import numpy as np import torch.nn as nn @@ -104,9 +104,7 @@ def argmax(X: torch.Tensor, dim=-1) -> torch.Tensor: return torch.argmax(X, dim=dim) @staticmethod - def check_all_layers_trainable(model: nn.Module) -> bool: + def get_non_trainable(model: nn.Module) -> List[Union[int, str]]: """Checks that all layers in a model are trainable.""" - for param in model.parameters(): - if not param.requires_grad: - return False - return True + return [name if name else i for i, (name, param) in enumerate(model.named_parameters()) + if not param.requires_grad] diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index b2a1430ba..0fa64a866 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -4,7 +4,7 @@ backend in order to ensure that the similarity methods only require to match this interface. """ -from typing import Callable, Optional, Union +from typing import Callable, Optional, Union, List import numpy as np import tensorflow as tf @@ -100,13 +100,9 @@ def argmax(X: tf.Tensor, dim=-1) -> tf.Tensor: return X @staticmethod - def check_all_layers_trainable(model: keras.Model) -> bool: + def get_non_trainable(model: keras.Model) -> List[Union[int, str]]: """Checks if all layers in a model are trainable. Note: batch normalization layers are ignored as they are not trainable by default. """ - for weight in model.non_trainable_weights: - if weight.name.startswith('batch_normalization'): - continue - return False - return True + return [getattr(layer, 'name', i) for i, layer in enumerate(model.layers) if not layer.trainable] diff --git a/alibi/explainers/similarity/grad.py b/alibi/explainers/similarity/grad.py index c14ceefc8..c30e426ad 100644 --- a/alibi/explainers/similarity/grad.py +++ b/alibi/explainers/similarity/grad.py @@ -129,9 +129,13 @@ def __init__(self, task_name=task ) - if not self.backend.check_all_layers_trainable(self.predictor): - warnings.warn(("Some layers in the model are not trainable. These layer gradients will not be " - "included in the computation of gradient similarity.")) + non_trainable_layers = self.backend.get_non_trainable(self.predictor) + if non_trainable_layers: + layers_msg = 'The following layers are not trainable: ' + layers = ", ".join([f"'{layer}'" for layer in non_trainable_layers if layer is not None]) + warning_msg = ("Some layers in the model are not trainable. These layer gradients will not be " + f"included in the computation of gradient similarity. {layers_msg}{layers}") + warnings.warn(warning_msg) def fit(self, X_train: np.ndarray, diff --git a/alibi/explainers/tests/test_simiarlity/conftest.py b/alibi/explainers/tests/test_simiarlity/conftest.py index 547391783..ab309af3f 100644 --- a/alibi/explainers/tests/test_simiarlity/conftest.py +++ b/alibi/explainers/tests/test_simiarlity/conftest.py @@ -155,6 +155,7 @@ def tf_linear_model(input_shape, output_shape, batch_norm=False): if batch_norm: layers.append(tf.keras.layers.BatchNormalization()) layers.extend([ + tf.keras.layers.Dense(output_shape), tf.keras.layers.Dense(output_shape), tf.keras.layers.Softmax() ]) @@ -175,6 +176,7 @@ def __init__(self, input_shape, output_shape): nn.Flatten(start_dim=1), nn.BatchNorm1d(input_shape) if batch_norm else nn.Identity(), nn.Linear(input_shape, output_shape), + nn.Linear(output_shape, output_shape), nn.Softmax() ) diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index fafa622db..ec2816e6c 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -348,8 +348,10 @@ def test_non_trainable_layer_warnings(linear_cls_model): if backend == 'pytorch': model.linear_stack[2].weight.requires_grad = False + model.linear_stack[3].weight.requires_grad = False elif backend == 'tensorflow': model.layers[1].trainable = False + model.layers[2].trainable = False with pytest.warns(Warning) as record: GradientSimilarity( @@ -358,5 +360,13 @@ def test_non_trainable_layer_warnings(linear_cls_model): loss_fn=loss_fn, backend=backend, ) - assert str(record[0].message) == ("Some layers in the model are not trainable. These layer gradients will not be " - "included in the computation of gradient similarity.") + + assert ("Some layers in the model are not trainable. These layer gradients will not be " + "included in the computation of gradient similarity.") in str(record[0].message) + + if backend == 'pytorch': + assert "The following layers are not trainable: 'linear_stack.2.weight', 'linear_stack.3.weight'" \ + in str(record[0].message) + if backend == 'tensorflow': + assert "The following layers are not trainable: 'dense_24', 'dense_25'" \ + in str(record[0].message) diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py index 05428da0b..f26c47d0a 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py @@ -74,7 +74,7 @@ def test_explainer_method_preprocessing(linear_cls_model, random_cls_dataset): assert X.shape == (1, 10) grad_X_test = explainer.backend.get_grads(model, X, Y, loss_fn) - assert grad_X_test.shape == (110, ) + assert grad_X_test.shape == (220, ) @pytest.mark.parametrize('linear_cls_model', From e77f67fd1cf25a0630a7fcfd32ee05c5c4e393ee Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 13:51:01 +0000 Subject: [PATCH 11/35] Add error for non-trainable models in GradSim method --- .../similarity/backends/pytorch/base.py | 15 +- .../similarity/backends/tensorflow/base.py | 12 +- alibi/explainers/similarity/grad.py | 4 +- .../tests/test_simiarlity/conftest.py | 18 +-- .../test_grad_methods_integration.py | 149 +++++++++++++----- .../test_simiarlity/test_grad_methods_unit.py | 2 +- 6 files changed, 141 insertions(+), 59 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index de6d5466d..b6f7251bb 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -67,6 +67,7 @@ def _grad_to_numpy(grad: torch.Tensor, name: Optional[str] = None) -> torch.Tens if not hasattr(grad, 'numpy'): name = f' for the named tensor: {name}' if name else '' + # add comment for devs raise TypeError((f'Could not convert gradient to numpy array{name}. To ignore these ' 'gradients in the similarity computation use `requires_grad=False`.')) return grad.reshape(-1).cpu().numpy() @@ -104,7 +105,15 @@ def argmax(X: torch.Tensor, dim=-1) -> torch.Tensor: return torch.argmax(X, dim=dim) @staticmethod - def get_non_trainable(model: nn.Module) -> List[Union[int, str]]: + def get_non_trainable(model: nn.Module) -> List[Optional[str]]: """Checks that all layers in a model are trainable.""" - return [name if name else i for i, (name, param) in enumerate(model.named_parameters()) - if not param.requires_grad] + + params = [name if name else None for name, param in model.named_parameters() + if not param.requires_grad] + + if len(params) == len(list(model.parameters())): + raise ValueError('The model has no trainable parameters. This method requires at least' + 'one trainable parameter to compute the gradients for. ' + 'Set `.requires_grad_(True)` on the model') + + return params diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index 0fa64a866..59519550a 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -8,7 +8,7 @@ import numpy as np import tensorflow as tf -import tensorflow.keras as keras +from tensorflow import keras class _TensorFlowBackend: @@ -100,9 +100,15 @@ def argmax(X: tf.Tensor, dim=-1) -> tf.Tensor: return X @staticmethod - def get_non_trainable(model: keras.Model) -> List[Union[int, str]]: + def get_non_trainable(model: keras.Model) -> List[Optional[str]]: """Checks if all layers in a model are trainable. Note: batch normalization layers are ignored as they are not trainable by default. """ - return [getattr(layer, 'name', i) for i, layer in enumerate(model.layers) if not layer.trainable] + + if len(model.trainable_weights) == 0: + raise ValueError('The model has no trainable weights. This method requires at least' + 'one trainable parameter to compute the gradients for. ' + 'Set `trainable=True` on the model or a model weight') + + return [getattr(weight, 'name', None) for weight in model.non_trainable_weights] diff --git a/alibi/explainers/similarity/grad.py b/alibi/explainers/similarity/grad.py index c30e426ad..68d782b0a 100644 --- a/alibi/explainers/similarity/grad.py +++ b/alibi/explainers/similarity/grad.py @@ -131,11 +131,11 @@ def __init__(self, non_trainable_layers = self.backend.get_non_trainable(self.predictor) if non_trainable_layers: - layers_msg = 'The following layers are not trainable: ' + layers_msg = 'The following tensors are not trainable: ' layers = ", ".join([f"'{layer}'" for layer in non_trainable_layers if layer is not None]) warning_msg = ("Some layers in the model are not trainable. These layer gradients will not be " f"included in the computation of gradient similarity. {layers_msg}{layers}") - warnings.warn(warning_msg) + warnings.warn(warning_msg) # todo: scope warning to this location def fit(self, X_train: np.ndarray, diff --git a/alibi/explainers/tests/test_simiarlity/conftest.py b/alibi/explainers/tests/test_simiarlity/conftest.py index ab309af3f..a17635f6f 100644 --- a/alibi/explainers/tests/test_simiarlity/conftest.py +++ b/alibi/explainers/tests/test_simiarlity/conftest.py @@ -82,11 +82,10 @@ def linear_cls_model(request): input_shape = request.param.get('input_shape', (10,)) output_shape = request.param.get('output_shape', 10) framework = request.param.get('framework', 'tensorflow') - batch_norm = request.param.get('batch_norm', False) model = { - 'tensorflow': lambda i_shape, o_shape: tf_linear_model(i_shape, o_shape, batch_norm), - 'pytorch': lambda i_shape, o_shape: torch_linear_model(i_shape, o_shape, batch_norm) + 'tensorflow': lambda i_shape, o_shape: tf_linear_model(i_shape, o_shape), + 'pytorch': lambda i_shape, o_shape: torch_linear_model(i_shape, o_shape) }[framework](input_shape, output_shape) loss_fn = { @@ -144,26 +143,21 @@ def linear_models(request): return tf_model, tf_loss, torch_model, torch_loss -def tf_linear_model(input_shape, output_shape, batch_norm=False): +def tf_linear_model(input_shape, output_shape): """ Constructs a linear model for `tensorflow`. """ layers = [ tf.keras.layers.InputLayer(input_shape=input_shape), tf.keras.layers.Flatten(), - ] - if batch_norm: - layers.append(tf.keras.layers.BatchNormalization()) - layers.extend([ - tf.keras.layers.Dense(output_shape), tf.keras.layers.Dense(output_shape), tf.keras.layers.Softmax() - ]) + ] return keras.Sequential(layers) -def torch_linear_model(input_shape_arg, output_shape_arg, batch_norm=False): +def torch_linear_model(input_shape_arg, output_shape_arg): """ Constructs a linear model for `torch`. """ @@ -174,9 +168,7 @@ def __init__(self, input_shape, output_shape): super(Model, self).__init__() self.linear_stack = nn.Sequential( nn.Flatten(start_dim=1), - nn.BatchNorm1d(input_shape) if batch_norm else nn.Identity(), nn.Linear(input_shape, output_shape), - nn.Linear(output_shape, output_shape), nn.Softmax() ) diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index ec2816e6c..0929a9a85 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -12,10 +12,11 @@ """ import pytest -import warnings import numpy as np import torch.nn as nn +import torch +import tensorflow as tf from tensorflow import keras from alibi.explainers.similarity.grad import GradientSimilarity @@ -313,60 +314,134 @@ def test_multiple_test_instances_stored_grads_asym_dot(precompute_grads): np.testing.assert_allclose(explanation.scores, scores[:, ::-1], atol=1e-4) -@pytest.mark.parametrize('linear_cls_model', - [ - ({'framework': 'pytorch', 'input_shape': (10,), 'output_shape': 10, 'batch_norm': True}), - ({'framework': 'tensorflow', 'input_shape': (10,), 'output_shape': 10, 'batch_norm': True}) - ], - indirect=True, ids=['torch-model', 'tf-model']) -def test_no_warnings_batch_norm(linear_cls_model): - """ - Test that no warnings are raised when using batch norm layers. +def test_non_trainable_layer_warnings_tf(): + """Test Non trainable layer warnings tensorflow. + + Test that warnings are raised when user passes non-trainbable layers to grad sim method for + tensorflow models. """ - backend, model, loss_fn, target_fn = linear_cls_model - with warnings.catch_warnings(): - warnings.simplefilter("error") + model = keras.Sequential([ + keras.layers.Dense(10), + keras.layers.Dense(20), + keras.layers.BatchNormalization(), + keras.Sequential([ + keras.layers.Dense(30), + keras.layers.Dense(40), + ]) + ]) + model.build((None, 10)) + + model.layers[1].trainable = False + model.layers[-1].layers[1].trainable = False + + tensor_names = [tensor.name for tensor in model.layers[1].non_trainable_weights] + tensor_names.extend([tensor.name for tensor in model.layers[-1].layers[1].non_trainable_weights]) + tensor_names.extend([tensor.name for tensor in model.layers[2].non_trainable_weights]) + + loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) + + with pytest.warns(Warning) as record: GradientSimilarity( model, task='classification', loss_fn=loss_fn, - backend=backend, + backend='tensorflow', ) + assert ("Some layers in the model are not trainable. These layer gradients will not be " + "included in the computation of gradient similarity.") in str(record[0].message) -@pytest.mark.parametrize('linear_cls_model', - [ - ({'framework': 'pytorch', 'input_shape': (10,), 'output_shape': 10}), - ({'framework': 'tensorflow', 'input_shape': (10,), 'output_shape': 10}) - ], - indirect=True, ids=['torch-model', 'tf-model']) -def test_non_trainable_layer_warnings(linear_cls_model): - """ - Test that warnings are raised when user passes non-trainbable layers to grad sim method. + assert "The following tensors are not trainable:" \ + in str(record[0].message) + for tensor_name in tensor_names: + # print(tensor_name, str(record[0].message).split(':')[-1]) + assert tensor_name in str(record[0].message) + + +def test_non_trainable_layer_warnings_pt(): + """Test Non trainable layer warnings pytorch. + + Test that warnings are raised when user passes non-trainbable layers to grad sim method for + pytorch models. """ - backend, model, loss_fn, target_fn = linear_cls_model - if backend == 'pytorch': - model.linear_stack[2].weight.requires_grad = False - model.linear_stack[3].weight.requires_grad = False - elif backend == 'tensorflow': - model.layers[1].trainable = False - model.layers[2].trainable = False + class Model(nn.Module): + def __init__(self, input_shape, output_shape): + super(Model, self).__init__() + self.linear_stack = nn.Sequential( + nn.Flatten(start_dim=1), + nn.Linear(input_shape, output_shape), + nn.Linear(output_shape, output_shape), + nn.BatchNorm1d(output_shape), + nn.Sequential( + nn.Linear(output_shape, output_shape), + nn.Linear(output_shape, output_shape) + ), + nn.Softmax() + ) + + def forward(self, x): + x = x.type(torch.FloatTensor) + return self.linear_stack(x) + + model = Model(10, 20) + model.linear_stack[2].weight.requires_grad = False + model.linear_stack[4][1].weight.requires_grad = False + tensor_names = [name for name, tensor in model.named_parameters() if not tensor.requires_grad] + + loss_fn = nn.CrossEntropyLoss() with pytest.warns(Warning) as record: GradientSimilarity( model, task='classification', loss_fn=loss_fn, - backend=backend, + backend='pytorch' ) assert ("Some layers in the model are not trainable. These layer gradients will not be " "included in the computation of gradient similarity.") in str(record[0].message) - if backend == 'pytorch': - assert "The following layers are not trainable: 'linear_stack.2.weight', 'linear_stack.3.weight'" \ - in str(record[0].message) - if backend == 'tensorflow': - assert "The following layers are not trainable: 'dense_24', 'dense_25'" \ - in str(record[0].message) + assert "The following tensors are not trainable:" \ + in str(record[0].message) + for tensor_name in tensor_names: + assert tensor_name in str(record[0].message) + + +def test_not_trainable_model_error_tf(): + """Test Not trainable model error tensorflow.""" + + model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + model.build((None, 2)) + model.trainable = False + with pytest.raises(ValueError) as err: + GradientSimilarity( + model, + task='regression', + loss_fn=loss_tf, + sim_fn='grad_dot', + backend='tensorflow' + ) + assert err.value.args[0] == ('The model has no trainable weights. This method requires at least' + 'one trainable parameter to compute the gradients for. ' + 'Set `trainable=True` on the model or a model weight') + + +def test_not_trainable_model_error_torch(): + """Test Not trainable model error pytorch.""" + + model = nn.Linear(2, 1, bias=False) + model.requires_grad_(False) + + with pytest.raises(ValueError) as err: + GradientSimilarity( + model, + task='regression', + loss_fn=loss_tf, + sim_fn='grad_dot', + backend='pytorch' + ) + + assert err.value.args[0] == ('The model has no trainable parameters. This method requires at least' + 'one trainable parameter to compute the gradients for. ' + 'Set `.requires_grad_(True)` on the model') diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py index f26c47d0a..05428da0b 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py @@ -74,7 +74,7 @@ def test_explainer_method_preprocessing(linear_cls_model, random_cls_dataset): assert X.shape == (1, 10) grad_X_test = explainer.backend.get_grads(model, X, Y, loss_fn) - assert grad_X_test.shape == (220, ) + assert grad_X_test.shape == (110, ) @pytest.mark.parametrize('linear_cls_model', From efde10237f5a16533bf3d4ad123e33ed1f4ba343 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 14:10:41 +0000 Subject: [PATCH 12/35] Rewrite get_non_trainable method docstrings --- alibi/explainers/similarity/backends/pytorch/base.py | 6 +++++- alibi/explainers/similarity/backends/tensorflow/base.py | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index b6f7251bb..72eac8480 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -106,7 +106,11 @@ def argmax(X: torch.Tensor, dim=-1) -> torch.Tensor: @staticmethod def get_non_trainable(model: nn.Module) -> List[Optional[str]]: - """Checks that all layers in a model are trainable.""" + """Returns a list of non trainable parameters. + + Returns a list of names of parameters that are non trainable. If no trainable parameter exist we raise + a ValueError. + """ params = [name if name else None for name, param in model.named_parameters() if not param.requires_grad] diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index 59519550a..a622aa774 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -101,9 +101,10 @@ def argmax(X: tf.Tensor, dim=-1) -> tf.Tensor: @staticmethod def get_non_trainable(model: keras.Model) -> List[Optional[str]]: - """Checks if all layers in a model are trainable. + """Returns a list of non trainable parameters. - Note: batch normalization layers are ignored as they are not trainable by default. + Returns a list of names of parameters that are non trainable. If no trainable parameter exist we raise + a ValueError. """ if len(model.trainable_weights) == 0: From 22dc19a76327ddbcbd86a2424fe273fe1e2233d0 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 14:18:57 +0000 Subject: [PATCH 13/35] Remove typos from doctstrings --- alibi/explainers/similarity/backends/pytorch/base.py | 2 +- alibi/explainers/similarity/backends/tensorflow/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index 72eac8480..e10299ddf 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -59,7 +59,7 @@ def get_grads( def _grad_to_numpy(grad: torch.Tensor, name: Optional[str] = None) -> torch.Tensor: """Convert graidient to numpy array. - Converts gradient tensor to flat numpy array. If the gradient is a sparse tensor, it is converted to a dense \ + Converts gradient tensor to flat numpy array. If the gradient is a sparse tensor, it is converted to a dense tensor first. """ if grad.is_sparse: diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index a622aa774..860389f6a 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -58,7 +58,7 @@ def get_grads( def _grad_to_numpy(grad: tf.Tensor, name: Optional[str] = None) -> tf.Tensor: """Convert graidient to numpy array. - Converts gradient tensor to flat numpy array. If the gradient is a sparse tensor, it is converted to a dense \ + Converts gradient tensor to flat numpy array. If the gradient is a sparse tensor, it is converted to a dense tensor first. """ From 557af3b13dfa0e56b5c6349029c83ac6b92ad180 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 14:26:02 +0000 Subject: [PATCH 14/35] Minor warning rephrase and update tests --- alibi/explainers/similarity/grad.py | 5 +++-- .../test_simiarlity/test_grad_methods_integration.py | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/alibi/explainers/similarity/grad.py b/alibi/explainers/similarity/grad.py index 68d782b0a..3fe8cb52a 100644 --- a/alibi/explainers/similarity/grad.py +++ b/alibi/explainers/similarity/grad.py @@ -133,8 +133,9 @@ def __init__(self, if non_trainable_layers: layers_msg = 'The following tensors are not trainable: ' layers = ", ".join([f"'{layer}'" for layer in non_trainable_layers if layer is not None]) - warning_msg = ("Some layers in the model are not trainable. These layer gradients will not be " - f"included in the computation of gradient similarity. {layers_msg}{layers}") + warning_msg = ("Some layers in the model are not trainable. These layers don't have gradients " + "and will not be included in the computation of gradient similarity. " + f"{layers_msg}{layers}") warnings.warn(warning_msg) # todo: scope warning to this location def fit(self, diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index 0929a9a85..3750053b8 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -348,8 +348,9 @@ def test_non_trainable_layer_warnings_tf(): backend='tensorflow', ) - assert ("Some layers in the model are not trainable. These layer gradients will not be " - "included in the computation of gradient similarity.") in str(record[0].message) + assert ("Some layers in the model are not trainable. These layers don't have " + "gradients and will not be included in the computation of gradient similarity.") \ + in str(record[0].message) assert "The following tensors are not trainable:" \ in str(record[0].message) @@ -399,8 +400,9 @@ def forward(self, x): backend='pytorch' ) - assert ("Some layers in the model are not trainable. These layer gradients will not be " - "included in the computation of gradient similarity.") in str(record[0].message) + assert ("Some layers in the model are not trainable. These layers don't have " + "gradients and will not be included in the computation of gradient similarity.") \ + in str(record[0].message) assert "The following tensors are not trainable:" \ in str(record[0].message) From b9c8f61d7622df0c774709f2761856ebd7d2bca1 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 14:27:09 +0000 Subject: [PATCH 15/35] Fix minor flake8 error --- .../tests/test_simiarlity/test_grad_methods_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index 3750053b8..00d884908 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -350,7 +350,7 @@ def test_non_trainable_layer_warnings_tf(): assert ("Some layers in the model are not trainable. These layers don't have " "gradients and will not be included in the computation of gradient similarity.") \ - in str(record[0].message) + in str(record[0].message) assert "The following tensors are not trainable:" \ in str(record[0].message) @@ -402,7 +402,7 @@ def forward(self, x): assert ("Some layers in the model are not trainable. These layers don't have " "gradients and will not be included in the computation of gradient similarity.") \ - in str(record[0].message) + in str(record[0].message) assert "The following tensors are not trainable:" \ in str(record[0].message) From 6015d509b4bb7df104335471c406c0821eff4a3c Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 14:59:16 +0000 Subject: [PATCH 16/35] Improve error message for numpy conversion error --- .../explainers/similarity/backends/pytorch/base.py | 4 ++-- .../similarity/backends/tensorflow/base.py | 3 ++- .../tests/test_simiarlity/test_backends.py | 14 +++++++++----- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index e10299ddf..d1a38d5d7 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -67,9 +67,9 @@ def _grad_to_numpy(grad: torch.Tensor, name: Optional[str] = None) -> torch.Tens if not hasattr(grad, 'numpy'): name = f' for the named tensor: {name}' if name else '' - # add comment for devs raise TypeError((f'Could not convert gradient to numpy array{name}. To ignore these ' - 'gradients in the similarity computation use `requires_grad=False`.')) + 'gradients in the similarity computation set `requires_grad=False` on the ' + 'corresponding parameter.')) return grad.reshape(-1).cpu().numpy() @staticmethod diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index 860389f6a..a0c8cf0e5 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -69,7 +69,8 @@ def _grad_to_numpy(grad: tf.Tensor, name: Optional[str] = None) -> tf.Tensor: if not hasattr(grad, 'numpy'): name = f' for the named tensor: {name}' if name else '' raise TypeError((f'Could not convert gradient to numpy array{name}. To ignore these ' - 'gradients in the similarity computation use `trainable=False`.')) + 'gradients in the similarity computation set `trainable=False` on the ' + 'corresponding parameter.')) return grad.numpy().reshape(-1) @staticmethod diff --git a/alibi/explainers/tests/test_simiarlity/test_backends.py b/alibi/explainers/tests/test_simiarlity/test_backends.py index 72a401654..6f7162aa2 100644 --- a/alibi/explainers/tests/test_simiarlity/test_backends.py +++ b/alibi/explainers/tests/test_simiarlity/test_backends.py @@ -100,14 +100,16 @@ class MockTensor(): with pytest.raises(TypeError) as err: _PytorchBackend._grad_to_numpy(MockTensor()) - assert ("Could not convert gradient to numpy array. To ignore these gradients in" - " the similarity computation use `requires_grad=False`.") in str(err.value) + assert ("Could not convert gradient to numpy array. To ignore these gradients in the " + "similarity computation set `requires_grad=False` on the corresponding parameter.") \ + in str(err.value) with pytest.raises(TypeError) as err: _PytorchBackend._grad_to_numpy(MockTensor(), 'test') assert ("Could not convert gradient to numpy array for the named tensor: test. " - "To ignore these gradients in the similarity computation use `requires_grad=False`.") in str(err.value) + "To ignore these gradients in the similarity computation set `requires_grad=False`" + " on the corresponding parameter.") in str(err.value) def test_non_numpy_grads_tensorflow(): @@ -122,10 +124,12 @@ class MockTensor(): _TensorFlowBackend._grad_to_numpy(MockTensor()) assert ("Could not convert gradient to numpy array. To ignore these gradients " - "in the similarity computation use `trainable=False`.") in str(err.value) + "in the similarity computation set `trainable=False` on the corresponding parameter.") \ + in str(err.value) with pytest.raises(TypeError) as err: _TensorFlowBackend._grad_to_numpy(MockTensor(), 'test') assert ("Could not convert gradient to numpy array for the named tensor: test." - " To ignore these gradients in the similarity computation use `trainable=False`.") in str(err.value) + " To ignore these gradients in the similarity computation set " + "`trainable=False` on the corresponding parameter.") in str(err.value) From a3270be4388164d344b946388c30bffed2903d48 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 15:09:33 +0000 Subject: [PATCH 17/35] Update tensorflow _grad_to_numpy type hints --- alibi/explainers/similarity/backends/tensorflow/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index a0c8cf0e5..8c164ca33 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -55,7 +55,7 @@ def get_grads( return grad_X_train @staticmethod - def _grad_to_numpy(grad: tf.Tensor, name: Optional[str] = None) -> tf.Tensor: + def _grad_to_numpy(grad: Union[tf.IndexedSlices, tf.Tensor], name: Optional[str] = None) -> tf.Tensor: """Convert graidient to numpy array. Converts gradient tensor to flat numpy array. If the gradient is a sparse tensor, it is converted to a dense From 188e1d3ed265f4c1638abd8bfa73466262dd1e57 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 15:34:14 +0000 Subject: [PATCH 18/35] Revert minor changes --- alibi/explainers/tests/test_simiarlity/conftest.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/alibi/explainers/tests/test_simiarlity/conftest.py b/alibi/explainers/tests/test_simiarlity/conftest.py index a17635f6f..4eb407e3f 100644 --- a/alibi/explainers/tests/test_simiarlity/conftest.py +++ b/alibi/explainers/tests/test_simiarlity/conftest.py @@ -147,14 +147,12 @@ def tf_linear_model(input_shape, output_shape): """ Constructs a linear model for `tensorflow`. """ - layers = [ + return keras.Sequential([ tf.keras.layers.InputLayer(input_shape=input_shape), tf.keras.layers.Flatten(), tf.keras.layers.Dense(output_shape), tf.keras.layers.Softmax() - ] - - return keras.Sequential(layers) + ]) def torch_linear_model(input_shape_arg, output_shape_arg): From 2af3367d61c17def5c96eb94b3bb3802c293476b Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 15:37:48 +0000 Subject: [PATCH 19/35] Minor spelling fix to test docstring --- alibi/explainers/tests/test_simiarlity/test_backends.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alibi/explainers/tests/test_simiarlity/test_backends.py b/alibi/explainers/tests/test_simiarlity/test_backends.py index 6f7162aa2..cd9fb7fd6 100644 --- a/alibi/explainers/tests/test_simiarlity/test_backends.py +++ b/alibi/explainers/tests/test_simiarlity/test_backends.py @@ -46,7 +46,7 @@ def test_backends(random_cls_dataset, linear_models): @pytest.mark.parametrize('trainable_emd, grads_shape', [(True, (61, )), (False, (21, ))]) def test_tf_embedding_similarity(trainable_emd, grads_shape): - """Test GradSim explainer correctly handles sparcity and non-trainable layers for tensorflow. + """Test GradSim explainer correctly handles sparsity and non-trainable layers for tensorflow. Test that `tensorflow` embedding layers work as expected and also that layers marked as non-trainable are not included in the gradients. @@ -68,7 +68,7 @@ def test_tf_embedding_similarity(trainable_emd, grads_shape): @pytest.mark.parametrize('trainable_emd, grads_shape', [(True, (61, )), (False, (21, ))]) @pytest.mark.parametrize('sparse', [True, False]) def test_pytorch_embedding_similarity(trainable_emd, grads_shape, sparse): - """Test GradSim explainer correctly handles sparcity and non-trainable layers for pytorch. + """Test GradSim explainer correctly handles sparsity and non-trainable layers for pytorch. Tests that the `pytorch` embedding layers work as expected and that layers marked as non-trainable are not included in the gradients. From a6e437c95e53e86c1d729020e7a05610102cbe50 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 15:41:51 +0000 Subject: [PATCH 20/35] Update test comment to make behavour clearer --- alibi/explainers/tests/test_simiarlity/test_backends.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/alibi/explainers/tests/test_simiarlity/test_backends.py b/alibi/explainers/tests/test_simiarlity/test_backends.py index cd9fb7fd6..e7f3520ba 100644 --- a/alibi/explainers/tests/test_simiarlity/test_backends.py +++ b/alibi/explainers/tests/test_simiarlity/test_backends.py @@ -62,7 +62,7 @@ def test_tf_embedding_similarity(trainable_emd, grads_shape): Y = tf.random.uniform(shape=(1, 1), minval=0, maxval=10, dtype=tf.float32) loss_fn = tf.keras.losses.MeanSquaredError() tf_grads = _TensorFlowBackend.get_grads(model, X, Y, loss_fn) - assert tf_grads.shape == grads_shape # (4 * 10) + (5 * 4) + 1 + assert tf_grads.shape == grads_shape # (4 * 10) * trainable_emd + (5 * 4) + 1 @pytest.mark.parametrize('trainable_emd, grads_shape', [(True, (61, )), (False, (21, ))]) @@ -85,8 +85,8 @@ def test_pytorch_embedding_similarity(trainable_emd, grads_shape, sparse): X = torch.randint(0, 10, (1, 5)) Y = torch.randint(0, 10, (1, 1), dtype=torch.float32) loss_fn = torch.nn.MSELoss() - tf_grads = _PytorchBackend.get_grads(model, X, Y, loss_fn) - assert tf_grads.shape == grads_shape # (4 * 10) + (5 * 4) + 1 + pt_grads = _PytorchBackend.get_grads(model, X, Y, loss_fn) + assert pt_grads.shape == grads_shape # (4 * 10) * trainable_emd + (5 * 4) + 1 def test_non_numpy_grads_pytorch(): From ab23c059b39b0804a395acf39b8e65d1796b5e38 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 15:47:37 +0000 Subject: [PATCH 21/35] Fix typeo in test docstring --- .../tests/test_simiarlity/test_grad_methods_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index 00d884908..3c2d5cf38 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -317,7 +317,7 @@ def test_multiple_test_instances_stored_grads_asym_dot(precompute_grads): def test_non_trainable_layer_warnings_tf(): """Test Non trainable layer warnings tensorflow. - Test that warnings are raised when user passes non-trainbable layers to grad sim method for + Test that warnings are raised when user passes non-trainable layers to grad sim method for tensorflow models. """ model = keras.Sequential([ From e95aaf13a82cf1bf99604fb2a8e3345057cb2aa8 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 15:56:19 +0000 Subject: [PATCH 22/35] Add comment to test to explain build method call --- .../test_grad_methods_integration.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index 3c2d5cf38..26ea77586 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -140,7 +140,11 @@ def test_correct_grad_dot_sim_result_tf(seed, normed_ds): `tensorflow` backend. """ model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + + # GradSim method checks weights are trainable so we need to build the model before passing it to the + # method model.build((None, 2)) + explainer = GradientSimilarity( model, task='regression', @@ -163,7 +167,10 @@ def test_correct_grad_cos_sim_result_tf(seed, ds): `tensorflow` backend. """ model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + # GradSim method checks weights are trainable so we need to build the model before passing it to the + # method model.build((None, 2)) + explainer = GradientSimilarity( model, task='regression', @@ -186,7 +193,10 @@ def test_grad_dot_result_order_tf(seed): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + # GradSim method checks weights are trainable so we need to build the model before passing it to the + # method model.build((None, 2)) + explainer = GradientSimilarity( model, task='regression', @@ -207,7 +217,10 @@ def test_grad_cos_result_order_tf(seed): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + # GradSim method checks weights are trainable so we need to build the model before passing it to the + # method model.build((None, 2)) + explainer = GradientSimilarity( model, task='regression', @@ -228,7 +241,10 @@ def test_multiple_test_instances_grad_cos(precompute_grads): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + # GradSim method checks weights are trainable so we need to build the model before passing it to the + # method model.build((None, 2)) + explainer = GradientSimilarity( model, task='regression', @@ -255,7 +271,10 @@ def test_multiple_test_instances_grad_dot(precompute_grads): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + # GradSim method checks weights are trainable so we need to build the model before passing it to the + # method model.build((None, 2)) + explainer = GradientSimilarity( model, task='regression', @@ -279,7 +298,10 @@ def test_multiple_test_instances_stored_grads_asym_dot(precompute_grads): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + # GradSim method checks weights are trainable so we need to build the model before passing it to the + # method model.build((None, 2)) + explainer = GradientSimilarity( model, task='regression', @@ -329,6 +351,9 @@ def test_non_trainable_layer_warnings_tf(): keras.layers.Dense(40), ]) ]) + + # GradSim method checks weights are trainable so we need to build the model before passing it to the + # method model.build((None, 10)) model.layers[1].trainable = False @@ -414,6 +439,8 @@ def test_not_trainable_model_error_tf(): """Test Not trainable model error tensorflow.""" model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) + # GradSim method checks weights are trainable so we need to build the model before passing it to the + # method model.build((None, 2)) model.trainable = False with pytest.raises(ValueError) as err: From 6704c640e3c1dfcd38489ab909b9d85e7737d16d Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 16:29:51 +0000 Subject: [PATCH 23/35] Add note on batch norm layer in GradSim integration tests --- .../tests/test_simiarlity/test_grad_methods_integration.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index 26ea77586..8b0b1e6f3 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -341,6 +341,10 @@ def test_non_trainable_layer_warnings_tf(): Test that warnings are raised when user passes non-trainable layers to grad sim method for tensorflow models. + + Note: Keras batch norm layers register non-trainable weights by default and so will raise the + warning we test for here. This is different to the pytorch behavour which doesn't include + the batch norm parameters in model.parameters(). """ model = keras.Sequential([ keras.layers.Dense(10), From 876ea61fd06cd9fe8178635c7dc0633150e43549 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 16:31:27 +0000 Subject: [PATCH 24/35] Fix minor linting errors --- .../test_grad_methods_integration.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index 8b0b1e6f3..851ca938f 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -141,7 +141,7 @@ def test_correct_grad_dot_sim_result_tf(seed, normed_ds): """ model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradSim method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -167,7 +167,7 @@ def test_correct_grad_cos_sim_result_tf(seed, ds): `tensorflow` backend. """ model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradSim method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -193,7 +193,7 @@ def test_grad_dot_result_order_tf(seed): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradSim method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -217,7 +217,7 @@ def test_grad_cos_result_order_tf(seed): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradSim method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -241,7 +241,7 @@ def test_multiple_test_instances_grad_cos(precompute_grads): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradSim method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -271,7 +271,7 @@ def test_multiple_test_instances_grad_dot(precompute_grads): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradSim method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -298,7 +298,7 @@ def test_multiple_test_instances_stored_grads_asym_dot(precompute_grads): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradSim method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -356,7 +356,7 @@ def test_non_trainable_layer_warnings_tf(): ]) ]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradSim method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 10)) @@ -443,7 +443,7 @@ def test_not_trainable_model_error_tf(): """Test Not trainable model error tensorflow.""" model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradSim method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) model.trainable = False From bf7fad34e6f9733e441d114c2c5d6196debe3306 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 16:45:36 +0000 Subject: [PATCH 25/35] Add backticks to modules names in docstrings --- .../similarity/backends/pytorch/base.py | 16 ++++++++-------- .../similarity/backends/tensorflow/base.py | 12 ++++++------ alibi/explainers/similarity/grad.py | 2 +- .../tests/test_simiarlity/test_backends.py | 8 ++++---- .../test_grad_methods_integration.py | 2 +- .../test_simiarlity/test_grad_methods_unit.py | 4 ++-- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index d1a38d5d7..7cf37a9a2 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -57,9 +57,9 @@ def get_grads( @staticmethod def _grad_to_numpy(grad: torch.Tensor, name: Optional[str] = None) -> torch.Tensor: - """Convert graidient to numpy array. + """Convert graidient to `np.ndarray`. - Converts gradient tensor to flat numpy array. If the gradient is a sparse tensor, it is converted to a dense + Converts gradient tensor to flat `numpy` array. If the gradient is a sparse tensor, it is converted to a dense tensor first. """ if grad.is_sparse: @@ -67,7 +67,7 @@ def _grad_to_numpy(grad: torch.Tensor, name: Optional[str] = None) -> torch.Tens if not hasattr(grad, 'numpy'): name = f' for the named tensor: {name}' if name else '' - raise TypeError((f'Could not convert gradient to numpy array{name}. To ignore these ' + raise TypeError((f'Could not convert gradient to `numpy` array{name}. To ignore these ' 'gradients in the similarity computation set `requires_grad=False` on the ' 'corresponding parameter.')) return grad.reshape(-1).cpu().numpy() @@ -91,12 +91,12 @@ def set_device(device: Union[str, int, torch.device, None] = None) -> None: elif isinstance(device, torch.device): _PytorchBackend.device = device elif device is not None: - raise TypeError(("`device` must be a None, string, integer or " - f"torch.device object. Got {type(device)} instead.")) + raise TypeError(("`device` must be a `None`, `string`, `integer` or " + f"`torch.device` object. Got {type(device)} instead.")) @staticmethod def to_numpy(X: torch.Tensor) -> np.ndarray: - """Maps a `pytorch` tensor to a `numpy` array.""" + """Maps a `pytorch` tensor to `np.ndarray`.""" return X.detach().cpu().numpy() @staticmethod @@ -109,7 +109,7 @@ def get_non_trainable(model: nn.Module) -> List[Optional[str]]: """Returns a list of non trainable parameters. Returns a list of names of parameters that are non trainable. If no trainable parameter exist we raise - a ValueError. + a `ValueError`. """ params = [name if name else None for name, param in model.named_parameters() @@ -118,6 +118,6 @@ def get_non_trainable(model: nn.Module) -> List[Optional[str]]: if len(params) == len(list(model.parameters())): raise ValueError('The model has no trainable parameters. This method requires at least' 'one trainable parameter to compute the gradients for. ' - 'Set `.requires_grad_(True)` on the model') + "Try setting `.requires_grad_(True)` on the model or one of it's parameters") return params diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index 8c164ca33..c10a08b76 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -56,9 +56,9 @@ def get_grads( @staticmethod def _grad_to_numpy(grad: Union[tf.IndexedSlices, tf.Tensor], name: Optional[str] = None) -> tf.Tensor: - """Convert graidient to numpy array. + """Convert graidient to `np.ndarray`. - Converts gradient tensor to flat numpy array. If the gradient is a sparse tensor, it is converted to a dense + Converts gradient tensor to flat `numpy` array. If the gradient is a sparse tensor, it is converted to a dense tensor first. """ @@ -68,7 +68,7 @@ def _grad_to_numpy(grad: Union[tf.IndexedSlices, tf.Tensor], name: Optional[str] if not hasattr(grad, 'numpy'): name = f' for the named tensor: {name}' if name else '' - raise TypeError((f'Could not convert gradient to numpy array{name}. To ignore these ' + raise TypeError((f'Could not convert gradient to `numpy` array{name}. To ignore these ' 'gradients in the similarity computation set `trainable=False` on the ' 'corresponding parameter.')) return grad.numpy().reshape(-1) @@ -87,11 +87,11 @@ def set_device(device: Union[str, None] = None) -> None: if device is None or isinstance(device, str): _TensorFlowBackend.device = device else: - raise TypeError(f"`device` must be a string or None. Got {type(device)} instead.") + raise TypeError(f"`device` must be a `string` or `None`. Got {type(device)} instead.") @staticmethod def to_numpy(X: tf.Tensor) -> tf.Tensor: - """Converts a tensor to a `numpy` array.""" + """Converts a tensor to `np.ndarray`.""" return X.numpy() @staticmethod @@ -105,7 +105,7 @@ def get_non_trainable(model: keras.Model) -> List[Optional[str]]: """Returns a list of non trainable parameters. Returns a list of names of parameters that are non trainable. If no trainable parameter exist we raise - a ValueError. + a `ValueError`. """ if len(model.trainable_weights) == 0: diff --git a/alibi/explainers/similarity/grad.py b/alibi/explainers/similarity/grad.py index 3fe8cb52a..b285acdbc 100644 --- a/alibi/explainers/similarity/grad.py +++ b/alibi/explainers/similarity/grad.py @@ -143,7 +143,7 @@ def fit(self, Y_train: np.ndarray) -> "Explainer": """Fit the explainer. - The GradientSimilarity explainer requires the model gradients over the training data. In the explain method it + The `GradientSimilarity` explainer requires the model gradients over the training data. In the explain method it compares them to the model gradients for the test instance(s). If ``store_grads`` was set to ``True`` on initialization then the gradients are precomputed here and stored. This will speed up the explain method call but storing the gradients may not be feasible for large models. diff --git a/alibi/explainers/tests/test_simiarlity/test_backends.py b/alibi/explainers/tests/test_simiarlity/test_backends.py index e7f3520ba..3f82fcfd2 100644 --- a/alibi/explainers/tests/test_simiarlity/test_backends.py +++ b/alibi/explainers/tests/test_simiarlity/test_backends.py @@ -100,14 +100,14 @@ class MockTensor(): with pytest.raises(TypeError) as err: _PytorchBackend._grad_to_numpy(MockTensor()) - assert ("Could not convert gradient to numpy array. To ignore these gradients in the " + assert ("Could not convert gradient to `numpy` array. To ignore these gradients in the " "similarity computation set `requires_grad=False` on the corresponding parameter.") \ in str(err.value) with pytest.raises(TypeError) as err: _PytorchBackend._grad_to_numpy(MockTensor(), 'test') - assert ("Could not convert gradient to numpy array for the named tensor: test. " + assert ("Could not convert gradient to `numpy` array for the named tensor: test. " "To ignore these gradients in the similarity computation set `requires_grad=False`" " on the corresponding parameter.") in str(err.value) @@ -123,13 +123,13 @@ class MockTensor(): with pytest.raises(TypeError) as err: _TensorFlowBackend._grad_to_numpy(MockTensor()) - assert ("Could not convert gradient to numpy array. To ignore these gradients " + assert ("Could not convert gradient to `numpy` array. To ignore these gradients " "in the similarity computation set `trainable=False` on the corresponding parameter.") \ in str(err.value) with pytest.raises(TypeError) as err: _TensorFlowBackend._grad_to_numpy(MockTensor(), 'test') - assert ("Could not convert gradient to numpy array for the named tensor: test." + assert ("Could not convert gradient to `numpy` array for the named tensor: test." " To ignore these gradients in the similarity computation set " "`trainable=False` on the corresponding parameter.") in str(err.value) diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index 851ca938f..9f9ee775a 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -477,4 +477,4 @@ def test_not_trainable_model_error_torch(): assert err.value.args[0] == ('The model has no trainable parameters. This method requires at least' 'one trainable parameter to compute the gradients for. ' - 'Set `.requires_grad_(True)` on the model') + "Try setting `.requires_grad_(True)` on the model or one of it's parameters") diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py index 05428da0b..6f5bf33de 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py @@ -233,7 +233,7 @@ def test_device_error_msgs(linear_reg_model): device=[0] ) if backend == 'pytorch': - assert ("`device` must be a None, string, integer or torch.device object." + assert ("`device` must be a `None`, `string`, `integer` or `torch.device` object." " Got instead.") in str(err.value) elif backend == 'tensorflow': - assert "`device` must be a string or None. Got instead." in str(err.value) + assert "`device` must be a `string` or `None`. Got instead." in str(err.value) From 0e0c919749e3d39e9dceac48ac4a350796c5a67a Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 17:02:26 +0000 Subject: [PATCH 26/35] Make get_not_trainable private --- alibi/explainers/similarity/backends/pytorch/base.py | 2 +- alibi/explainers/similarity/backends/tensorflow/base.py | 2 +- alibi/explainers/similarity/grad.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index 7cf37a9a2..0c857c730 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -105,7 +105,7 @@ def argmax(X: torch.Tensor, dim=-1) -> torch.Tensor: return torch.argmax(X, dim=dim) @staticmethod - def get_non_trainable(model: nn.Module) -> List[Optional[str]]: + def _get_non_trainable(model: nn.Module) -> List[Optional[str]]: """Returns a list of non trainable parameters. Returns a list of names of parameters that are non trainable. If no trainable parameter exist we raise diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index c10a08b76..ad76a2ecd 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -101,7 +101,7 @@ def argmax(X: tf.Tensor, dim=-1) -> tf.Tensor: return X @staticmethod - def get_non_trainable(model: keras.Model) -> List[Optional[str]]: + def _get_non_trainable(model: keras.Model) -> List[Optional[str]]: """Returns a list of non trainable parameters. Returns a list of names of parameters that are non trainable. If no trainable parameter exist we raise diff --git a/alibi/explainers/similarity/grad.py b/alibi/explainers/similarity/grad.py index b285acdbc..c5da6074e 100644 --- a/alibi/explainers/similarity/grad.py +++ b/alibi/explainers/similarity/grad.py @@ -129,7 +129,7 @@ def __init__(self, task_name=task ) - non_trainable_layers = self.backend.get_non_trainable(self.predictor) + non_trainable_layers = self.backend._get_non_trainable(self.predictor) if non_trainable_layers: layers_msg = 'The following tensors are not trainable: ' layers = ", ".join([f"'{layer}'" for layer in non_trainable_layers if layer is not None]) From 09a1674c1d159f0b7245156e0c797286c2c86b26 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Mon, 9 Jan 2023 17:40:29 +0000 Subject: [PATCH 27/35] Add questions to the FAQ docs page detailing performance soln and warnings --- doc/source/overview/faq.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/doc/source/overview/faq.md b/doc/source/overview/faq.md index b48b1babd..af96d3fc6 100644 --- a/doc/source/overview/faq.md +++ b/doc/source/overview/faq.md @@ -64,3 +64,19 @@ Unfortunately, running this line means it's impossible to run explainers based o ### Why am I'm unable to restrict the features allowed to changed in [CounterfactualProto](../methods/CFProto.ipynb)? This is a known issue with the current implementation, see [here](https://github.com/SeldonIO/alibi/issues/327) and [here](https://github.com/SeldonIO/alibi/issues/366#issuecomment-820299804). It is currently blocked until we migrate the code to use TensorFlow 2.x constructs. In the meantime, it is recommended to use [CFRL](../methods/CFRL.ipynb) for counterfactual explanations with flexible feature-range constraints. + + +## Similarity explanations + +### I'm using the [GradientSimilarity](../methods/Similarity.ipynb) method on a large model and it runs very slow. If I use `precompute_grads` I get out of memory errors? + +Large models with many parameters result in the similarity method running very slow and using `precompute_grads=True` may not be an option due to the memory cost. The best solutions for this problem are: + +- Use the explainer on a reduced dataset. You can use [Prototype Methods](../methods/ProtoSelect.ipynb) to obtain a smaller representative sample. +- Freeze some parameters in the model so that when computing the gradients the simialrity method excludes them. If using [tensorflow](https://www.tensorflow.org/guide/keras/transfer_learning) you can do this by setting `trainable` to false on layers or specific parameters. For [torch](https://pytorch.org/docs/master/notes/autograd.html#locally-disabling-gradient-computation) we can set `requires_grad=False` on the relevent model parameters. + +Note that doing so will cause the explainer to issue a warning on initialization, informing you there are non-trainable parameters in your model and the explainer will not use those when computng the similarity scores. + +### I'm using the [GradientSimilarity](../methods/Similarity.ipynb) method on a tensorflow model and I keep getting warnings about non-trainable parameters but I haven't set any to be non-trainable? + +This warning likely means that your model has layers that track statistics using non-trainable parameters such as batch normalization layers. The warning should list the specific tensors that are non-trainable so you should be able to check. If this is the case you don't need to worry as similarity methods don't use those parameters anyway. Otherwise you will see this warning if you have set one of the parameters to `trainable=False` and alibi is just making sure you know this is the case. From f443185d71d75bedf6bac791bcfef8396baec00e Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Tue, 10 Jan 2023 09:03:18 +0000 Subject: [PATCH 28/35] Minor fixes --- alibi/explainers/similarity/backends/pytorch/base.py | 2 +- alibi/explainers/similarity/backends/tensorflow/base.py | 2 +- alibi/explainers/similarity/grad.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index 0c857c730..cdb79cd26 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -108,7 +108,7 @@ def argmax(X: torch.Tensor, dim=-1) -> torch.Tensor: def _get_non_trainable(model: nn.Module) -> List[Optional[str]]: """Returns a list of non trainable parameters. - Returns a list of names of parameters that are non trainable. If no trainable parameter exist we raise + Returns a list of names of parameters that are non trainable. If no trainable parameter exists we raise a `ValueError`. """ diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index ad76a2ecd..4ff843d08 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -104,7 +104,7 @@ def argmax(X: tf.Tensor, dim=-1) -> tf.Tensor: def _get_non_trainable(model: keras.Model) -> List[Optional[str]]: """Returns a list of non trainable parameters. - Returns a list of names of parameters that are non trainable. If no trainable parameter exist we raise + Returns a list of names of parameters that are non trainable. If no trainable parameter exists we raise a `ValueError`. """ diff --git a/alibi/explainers/similarity/grad.py b/alibi/explainers/similarity/grad.py index c5da6074e..078917ec7 100644 --- a/alibi/explainers/similarity/grad.py +++ b/alibi/explainers/similarity/grad.py @@ -45,7 +45,7 @@ def __init__(self, device: 'Union[int, str, torch.device, None]' = None, verbose: bool = False, ): - """GradientSimilarity explainer. + """``GradientSimilarity`` explainer. The gradient similarity explainer is used to find examples in the training data that the predictor considers similar to test instances the user wants to explain. It uses the gradients of the loss between the model output @@ -143,7 +143,7 @@ def fit(self, Y_train: np.ndarray) -> "Explainer": """Fit the explainer. - The `GradientSimilarity` explainer requires the model gradients over the training data. In the explain method it + The ``GradientSimilarity`` explainer requires the model gradients over the training data. In the explain method it compares them to the model gradients for the test instance(s). If ``store_grads`` was set to ``True`` on initialization then the gradients are precomputed here and stored. This will speed up the explain method call but storing the gradients may not be feasible for large models. From 8a99be5d30bd0169b112625f60819aad65bbe548 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Tue, 10 Jan 2023 09:10:34 +0000 Subject: [PATCH 29/35] Fix linting error --- alibi/explainers/similarity/grad.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alibi/explainers/similarity/grad.py b/alibi/explainers/similarity/grad.py index 078917ec7..9be94b1ee 100644 --- a/alibi/explainers/similarity/grad.py +++ b/alibi/explainers/similarity/grad.py @@ -143,8 +143,8 @@ def fit(self, Y_train: np.ndarray) -> "Explainer": """Fit the explainer. - The ``GradientSimilarity`` explainer requires the model gradients over the training data. In the explain method it - compares them to the model gradients for the test instance(s). If ``store_grads`` was set to ``True`` on + The ``GradientSimilarity`` explainer requires the model gradients over the training data. In the explain method + it compares them to the model gradients for the test instance(s). If ``store_grads`` was set to ``True`` on initialization then the gradients are precomputed here and stored. This will speed up the explain method call but storing the gradients may not be feasible for large models. From f69227b3276bf0354afdd179aabc15f522165dc6 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Tue, 10 Jan 2023 10:57:21 +0000 Subject: [PATCH 30/35] Add minor changes --- alibi/explainers/similarity/backends/pytorch/base.py | 4 ++-- alibi/explainers/similarity/backends/tensorflow/base.py | 6 +++--- alibi/explainers/similarity/grad.py | 2 +- doc/source/overview/faq.md | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index cdb79cd26..4bf30614a 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -56,8 +56,8 @@ def get_grads( if param.grad is not None]) @staticmethod - def _grad_to_numpy(grad: torch.Tensor, name: Optional[str] = None) -> torch.Tensor: - """Convert graidient to `np.ndarray`. + def _grad_to_numpy(grad: torch.Tensor, name: Optional[str] = None) -> np.ndarray: + """Convert gradient to `np.ndarray`. Converts gradient tensor to flat `numpy` array. If the gradient is a sparse tensor, it is converted to a dense tensor first. diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index 4ff843d08..9e79d5b01 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -55,8 +55,8 @@ def get_grads( return grad_X_train @staticmethod - def _grad_to_numpy(grad: Union[tf.IndexedSlices, tf.Tensor], name: Optional[str] = None) -> tf.Tensor: - """Convert graidient to `np.ndarray`. + def _grad_to_numpy(grad: Union[tf.IndexedSlices, tf.Tensor], name: Optional[str] = None) -> np.ndarray: + """Convert gradient to `np.ndarray`. Converts gradient tensor to flat `numpy` array. If the gradient is a sparse tensor, it is converted to a dense tensor first. @@ -90,7 +90,7 @@ def set_device(device: Union[str, None] = None) -> None: raise TypeError(f"`device` must be a `string` or `None`. Got {type(device)} instead.") @staticmethod - def to_numpy(X: tf.Tensor) -> tf.Tensor: + def to_numpy(X: tf.Tensor) -> np.ndarray: """Converts a tensor to `np.ndarray`.""" return X.numpy() diff --git a/alibi/explainers/similarity/grad.py b/alibi/explainers/similarity/grad.py index 9be94b1ee..0c5d2ab36 100644 --- a/alibi/explainers/similarity/grad.py +++ b/alibi/explainers/similarity/grad.py @@ -144,7 +144,7 @@ def fit(self, """Fit the explainer. The ``GradientSimilarity`` explainer requires the model gradients over the training data. In the explain method - it compares them to the model gradients for the test instance(s). If ``store_grads`` was set to ``True`` on + it compares them to the model gradients for the test instance(s). If ``precompute_grads`` was set to ``True`` on initialization then the gradients are precomputed here and stored. This will speed up the explain method call but storing the gradients may not be feasible for large models. diff --git a/doc/source/overview/faq.md b/doc/source/overview/faq.md index af96d3fc6..eb85cd70d 100644 --- a/doc/source/overview/faq.md +++ b/doc/source/overview/faq.md @@ -68,12 +68,12 @@ This is a known issue with the current implementation, see [here](https://github ## Similarity explanations -### I'm using the [GradientSimilarity](../methods/Similarity.ipynb) method on a large model and it runs very slow. If I use `precompute_grads` I get out of memory errors? +### I'm using the [GradientSimilarity](../methods/Similarity.ipynb) method on a large model and it runs very slow. If I use `precompute_grads=True` I get out of memory errors? Large models with many parameters result in the similarity method running very slow and using `precompute_grads=True` may not be an option due to the memory cost. The best solutions for this problem are: - Use the explainer on a reduced dataset. You can use [Prototype Methods](../methods/ProtoSelect.ipynb) to obtain a smaller representative sample. -- Freeze some parameters in the model so that when computing the gradients the simialrity method excludes them. If using [tensorflow](https://www.tensorflow.org/guide/keras/transfer_learning) you can do this by setting `trainable` to false on layers or specific parameters. For [torch](https://pytorch.org/docs/master/notes/autograd.html#locally-disabling-gradient-computation) we can set `requires_grad=False` on the relevent model parameters. +- Freeze some parameters in the model so that when computing the gradients the simialrity method excludes them. If using [tensorflow](https://www.tensorflow.org/guide/keras/transfer_learning) you can do this by setting `trainable=False` on layers or specific parameters. For [pytorch](https://pytorch.org/docs/master/notes/autograd.html#locally-disabling-gradient-computation) we can set `requires_grad=False` on the relevent model parameters. Note that doing so will cause the explainer to issue a warning on initialization, informing you there are non-trainable parameters in your model and the explainer will not use those when computng the similarity scores. From 42cf904a0532e604fa8af2b302f3e51fa2fad3ca Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Tue, 10 Jan 2023 11:35:59 +0000 Subject: [PATCH 31/35] Fix Requested PR changes --- .../similarity/backends/pytorch/base.py | 10 ++-- .../similarity/backends/tensorflow/base.py | 11 ++-- alibi/explainers/similarity/grad.py | 12 ++-- .../tests/test_simiarlity/test_backends.py | 20 +++---- .../test_grad_methods_integration.py | 56 +++++++++---------- .../test_simiarlity/test_grad_methods_unit.py | 4 +- doc/source/overview/faq.md | 2 +- 7 files changed, 57 insertions(+), 58 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index 4bf30614a..a3f7aa566 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -68,7 +68,7 @@ def _grad_to_numpy(grad: torch.Tensor, name: Optional[str] = None) -> np.ndarray if not hasattr(grad, 'numpy'): name = f' for the named tensor: {name}' if name else '' raise TypeError((f'Could not convert gradient to `numpy` array{name}. To ignore these ' - 'gradients in the similarity computation set `requires_grad=False` on the ' + 'gradients in the similarity computation set ``requires_grad=False`` on the ' 'corresponding parameter.')) return grad.reshape(-1).cpu().numpy() @@ -91,7 +91,7 @@ def set_device(device: Union[str, int, torch.device, None] = None) -> None: elif isinstance(device, torch.device): _PytorchBackend.device = device elif device is not None: - raise TypeError(("`device` must be a `None`, `string`, `integer` or " + raise TypeError(("`device` must be a ``None``, ``string``, ``integer`` or " f"`torch.device` object. Got {type(device)} instead.")) @staticmethod @@ -116,8 +116,8 @@ def _get_non_trainable(model: nn.Module) -> List[Optional[str]]: if not param.requires_grad] if len(params) == len(list(model.parameters())): - raise ValueError('The model has no trainable parameters. This method requires at least' - 'one trainable parameter to compute the gradients for. ' - "Try setting `.requires_grad_(True)` on the model or one of it's parameters") + raise ValueError("The model has no trainable parameters. This method requires at least " + "one trainable parameter to compute the gradients for. " + "Try setting ``.requires_grad_(True)`` on the model or one of its parameters.") return params diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index 9e79d5b01..9fd139595 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -69,7 +69,7 @@ def _grad_to_numpy(grad: Union[tf.IndexedSlices, tf.Tensor], name: Optional[str] if not hasattr(grad, 'numpy'): name = f' for the named tensor: {name}' if name else '' raise TypeError((f'Could not convert gradient to `numpy` array{name}. To ignore these ' - 'gradients in the similarity computation set `trainable=False` on the ' + 'gradients in the similarity computation set ``trainable=False`` on the ' 'corresponding parameter.')) return grad.numpy().reshape(-1) @@ -87,7 +87,7 @@ def set_device(device: Union[str, None] = None) -> None: if device is None or isinstance(device, str): _TensorFlowBackend.device = device else: - raise TypeError(f"`device` must be a `string` or `None`. Got {type(device)} instead.") + raise TypeError(f"`device` must be a ``string`` or ``None``. Got {type(device)} instead.") @staticmethod def to_numpy(X: tf.Tensor) -> np.ndarray: @@ -109,8 +109,7 @@ def _get_non_trainable(model: keras.Model) -> List[Optional[str]]: """ if len(model.trainable_weights) == 0: - raise ValueError('The model has no trainable weights. This method requires at least' - 'one trainable parameter to compute the gradients for. ' - 'Set `trainable=True` on the model or a model weight') - + raise ValueError("The model has no trainable weights. This method requires at least " + "one trainable parameter to compute the gradients for. " + "Set ``trainable=True`` on the model or a model weight.") return [getattr(weight, 'name', None) for weight in model.non_trainable_weights] diff --git a/alibi/explainers/similarity/grad.py b/alibi/explainers/similarity/grad.py index 0c5d2ab36..2b5707c51 100644 --- a/alibi/explainers/similarity/grad.py +++ b/alibi/explainers/similarity/grad.py @@ -45,12 +45,12 @@ def __init__(self, device: 'Union[int, str, torch.device, None]' = None, verbose: bool = False, ): - """``GradientSimilarity`` explainer. + """`GradientSimilarity` explainer. The gradient similarity explainer is used to find examples in the training data that the predictor considers similar to test instances the user wants to explain. It uses the gradients of the loss between the model output and the training data labels. These are compared using the similarity function specified by ``sim_fn``. The - GradientSimilarity can be applied to models trained for both classification and regression tasks. + `GradientSimilarity` explainer can be applied to models trained for both classification and regression tasks. Parameters @@ -131,9 +131,9 @@ def __init__(self, non_trainable_layers = self.backend._get_non_trainable(self.predictor) if non_trainable_layers: - layers_msg = 'The following tensors are not trainable: ' + layers_msg = 'The following tensors are non-trainable: ' layers = ", ".join([f"'{layer}'" for layer in non_trainable_layers if layer is not None]) - warning_msg = ("Some layers in the model are not trainable. These layers don't have gradients " + warning_msg = ("Some layers in the model are non-trainable. These layers don't have gradients " "and will not be included in the computation of gradient similarity. " f"{layers_msg}{layers}") warnings.warn(warning_msg) # todo: scope warning to this location @@ -143,8 +143,8 @@ def fit(self, Y_train: np.ndarray) -> "Explainer": """Fit the explainer. - The ``GradientSimilarity`` explainer requires the model gradients over the training data. In the explain method - it compares them to the model gradients for the test instance(s). If ``precompute_grads`` was set to ``True`` on + The `GradientSimilarity` explainer requires the model gradients over the training data. In the explain method + it compares them to the model gradients for the test instance(s). If `precompute_grads` was set to ``True`` on initialization then the gradients are precomputed here and stored. This will speed up the explain method call but storing the gradients may not be feasible for large models. diff --git a/alibi/explainers/tests/test_simiarlity/test_backends.py b/alibi/explainers/tests/test_simiarlity/test_backends.py index 3f82fcfd2..1755798dc 100644 --- a/alibi/explainers/tests/test_simiarlity/test_backends.py +++ b/alibi/explainers/tests/test_simiarlity/test_backends.py @@ -46,7 +46,7 @@ def test_backends(random_cls_dataset, linear_models): @pytest.mark.parametrize('trainable_emd, grads_shape', [(True, (61, )), (False, (21, ))]) def test_tf_embedding_similarity(trainable_emd, grads_shape): - """Test GradSim explainer correctly handles sparsity and non-trainable layers for tensorflow. + """Test `GradientSimilarity` explainer correctly handles sparsity and non-trainable layers for `tensorflow`. Test that `tensorflow` embedding layers work as expected and also that layers marked as non-trainable are not included in the gradients. @@ -68,7 +68,7 @@ def test_tf_embedding_similarity(trainable_emd, grads_shape): @pytest.mark.parametrize('trainable_emd, grads_shape', [(True, (61, )), (False, (21, ))]) @pytest.mark.parametrize('sparse', [True, False]) def test_pytorch_embedding_similarity(trainable_emd, grads_shape, sparse): - """Test GradSim explainer correctly handles sparsity and non-trainable layers for pytorch. + """Test GradientSimilarity explainer correctly handles sparsity and non-trainable layers for pytorch. Tests that the `pytorch` embedding layers work as expected and that layers marked as non-trainable are not included in the gradients. @@ -90,9 +90,9 @@ def test_pytorch_embedding_similarity(trainable_emd, grads_shape, sparse): def test_non_numpy_grads_pytorch(): - """Test that the `pytorch` backend handles non-numpy gradients correctly. + """Test that the `pytorch` backend handles gradients withtout `numpy` methods correctly. - _PytorchBackend should throw an error if the gradients cannot be converted to numpy arrays. + `_PytorchBackend` should throw an error if the gradients cannot be converted to numpy arrays. """ class MockTensor(): is_sparse = False @@ -101,21 +101,21 @@ class MockTensor(): _PytorchBackend._grad_to_numpy(MockTensor()) assert ("Could not convert gradient to `numpy` array. To ignore these gradients in the " - "similarity computation set `requires_grad=False` on the corresponding parameter.") \ + "similarity computation set ``requires_grad=False`` on the corresponding parameter.") \ in str(err.value) with pytest.raises(TypeError) as err: _PytorchBackend._grad_to_numpy(MockTensor(), 'test') assert ("Could not convert gradient to `numpy` array for the named tensor: test. " - "To ignore these gradients in the similarity computation set `requires_grad=False`" + "To ignore these gradients in the similarity computation set ``requires_grad=False``" " on the corresponding parameter.") in str(err.value) def test_non_numpy_grads_tensorflow(): - """Test that the `tensorflow` backend handles non-numpy gradients correctly. + """Test that the `tensorflow` backend handles gradients without `numpy` methods correctly. - _TensorFlowBackend should throw an error if the gradients cannot be converted to numpy arrays. + `_TensorFlowBackend` should throw an error if the gradients cannot be converted to `numpy` arrays. """ class MockTensor(): is_sparse = False @@ -124,7 +124,7 @@ class MockTensor(): _TensorFlowBackend._grad_to_numpy(MockTensor()) assert ("Could not convert gradient to `numpy` array. To ignore these gradients " - "in the similarity computation set `trainable=False` on the corresponding parameter.") \ + "in the similarity computation set ``trainable=False`` on the corresponding parameter.") \ in str(err.value) with pytest.raises(TypeError) as err: @@ -132,4 +132,4 @@ class MockTensor(): assert ("Could not convert gradient to `numpy` array for the named tensor: test." " To ignore these gradients in the similarity computation set " - "`trainable=False` on the corresponding parameter.") in str(err.value) + "``trainable=False`` on the corresponding parameter.") in str(err.value) diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index 9f9ee775a..e5b1cb7ae 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -141,7 +141,7 @@ def test_correct_grad_dot_sim_result_tf(seed, normed_ds): """ model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradientSimilarity method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -167,7 +167,7 @@ def test_correct_grad_cos_sim_result_tf(seed, ds): `tensorflow` backend. """ model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradientSimilarity method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -193,7 +193,7 @@ def test_grad_dot_result_order_tf(seed): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradientSimilarity method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -217,7 +217,7 @@ def test_grad_cos_result_order_tf(seed): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradientSimilarity method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -241,7 +241,7 @@ def test_multiple_test_instances_grad_cos(precompute_grads): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradientSimilarity method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -271,7 +271,7 @@ def test_multiple_test_instances_grad_dot(precompute_grads): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradientSimilarity method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -298,7 +298,7 @@ def test_multiple_test_instances_stored_grads_asym_dot(precompute_grads): """ ds = np.array([[1, 0], [0.9, 0.1], [0.5 * 100, 0.5 * 100]]).astype('float32') model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradientSimilarity method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) @@ -337,14 +337,14 @@ def test_multiple_test_instances_stored_grads_asym_dot(precompute_grads): def test_non_trainable_layer_warnings_tf(): - """Test Non trainable layer warnings tensorflow. + """Test non-trainable layer warnings `tensorflow`. - Test that warnings are raised when user passes non-trainable layers to grad sim method for - tensorflow models. + Test that warnings are raised when user passes non-trainable layers to `GradientSimilarity` method for + `tensorflow` models. - Note: Keras batch norm layers register non-trainable weights by default and so will raise the - warning we test for here. This is different to the pytorch behavour which doesn't include - the batch norm parameters in model.parameters(). + Note: `Keras` batch norm layers register non-trainable weights by default and so will raise the + warning we test for here. This is different to the `pytorch` behavour which doesn't include + the batch norm parameters in ``model.parameters()``. """ model = keras.Sequential([ keras.layers.Dense(10), @@ -356,7 +356,7 @@ def test_non_trainable_layer_warnings_tf(): ]) ]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradientSimilarity method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 10)) @@ -377,11 +377,11 @@ def test_non_trainable_layer_warnings_tf(): backend='tensorflow', ) - assert ("Some layers in the model are not trainable. These layers don't have " + assert ("Some layers in the model are non-trainable. These layers don't have " "gradients and will not be included in the computation of gradient similarity.") \ in str(record[0].message) - assert "The following tensors are not trainable:" \ + assert "The following tensors are non-trainable:" \ in str(record[0].message) for tensor_name in tensor_names: # print(tensor_name, str(record[0].message).split(':')[-1]) @@ -389,10 +389,10 @@ def test_non_trainable_layer_warnings_tf(): def test_non_trainable_layer_warnings_pt(): - """Test Non trainable layer warnings pytorch. + """Test non-trainable layer warnings `pytorch`. - Test that warnings are raised when user passes non-trainbable layers to grad sim method for - pytorch models. + Test that warnings are raised when user passes non-trainbable layers to to `GradientSimilarity` method for + `pytorch` models. """ class Model(nn.Module): @@ -429,21 +429,21 @@ def forward(self, x): backend='pytorch' ) - assert ("Some layers in the model are not trainable. These layers don't have " + assert ("Some layers in the model are non-trainable. These layers don't have " "gradients and will not be included in the computation of gradient similarity.") \ in str(record[0].message) - assert "The following tensors are not trainable:" \ + assert "The following tensors are non-trainable:" \ in str(record[0].message) for tensor_name in tensor_names: assert tensor_name in str(record[0].message) def test_not_trainable_model_error_tf(): - """Test Not trainable model error tensorflow.""" + """Test non-trainable model error `tensorflow`.""" model = keras.Sequential([keras.layers.Dense(1, use_bias=False)]) - # GradSim method checks weights are trainable so we need to build the model before passing it to the + # GradientSimilarity method checks weights are trainable so we need to build the model before passing it to the # method model.build((None, 2)) model.trainable = False @@ -455,13 +455,13 @@ def test_not_trainable_model_error_tf(): sim_fn='grad_dot', backend='tensorflow' ) - assert err.value.args[0] == ('The model has no trainable weights. This method requires at least' + assert err.value.args[0] == ('The model has no trainable weights. This method requires at least ' 'one trainable parameter to compute the gradients for. ' - 'Set `trainable=True` on the model or a model weight') + 'Set ``trainable=True`` on the model or a model weight.') def test_not_trainable_model_error_torch(): - """Test Not trainable model error pytorch.""" + """Test non-trainable model error `pytorch`.""" model = nn.Linear(2, 1, bias=False) model.requires_grad_(False) @@ -475,6 +475,6 @@ def test_not_trainable_model_error_torch(): backend='pytorch' ) - assert err.value.args[0] == ('The model has no trainable parameters. This method requires at least' + assert err.value.args[0] == ('The model has no trainable parameters. This method requires at least ' 'one trainable parameter to compute the gradients for. ' - "Try setting `.requires_grad_(True)` on the model or one of it's parameters") + "Try setting ``.requires_grad_(True)`` on the model or one of its parameters.") diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py index 6f5bf33de..571c7daa6 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py @@ -233,7 +233,7 @@ def test_device_error_msgs(linear_reg_model): device=[0] ) if backend == 'pytorch': - assert ("`device` must be a `None`, `string`, `integer` or `torch.device` object." + assert ("`device` must be a ``None``, ``string``, ``integer`` or `torch.device` object." " Got instead.") in str(err.value) elif backend == 'tensorflow': - assert "`device` must be a `string` or `None`. Got instead." in str(err.value) + assert "`device` must be a ``string`` or ``None``. Got instead." in str(err.value) diff --git a/doc/source/overview/faq.md b/doc/source/overview/faq.md index eb85cd70d..da77f82f9 100644 --- a/doc/source/overview/faq.md +++ b/doc/source/overview/faq.md @@ -68,7 +68,7 @@ This is a known issue with the current implementation, see [here](https://github ## Similarity explanations -### I'm using the [GradientSimilarity](../methods/Similarity.ipynb) method on a large model and it runs very slow. If I use `precompute_grads=True` I get out of memory errors? +### I'm using the [GradientSimilarity](../methods/Similarity.ipynb) method on a large model and it runs very slow. If I use `precompute_grads=True` I get out of memory errors. How do I solve this? Large models with many parameters result in the similarity method running very slow and using `precompute_grads=True` may not be an option due to the memory cost. The best solutions for this problem are: From 57d04b453e9b29c4b7134e7b6dbe0fcad260d638 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Tue, 10 Jan 2023 12:05:44 +0000 Subject: [PATCH 32/35] Update non-trainable params warning --- .../similarity/backends/pytorch/base.py | 15 ++++--- .../similarity/backends/tensorflow/base.py | 10 ++--- alibi/explainers/similarity/grad.py | 17 ++++---- .../test_grad_methods_integration.py | 39 ++++++++----------- 4 files changed, 38 insertions(+), 43 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index a3f7aa566..50f45d34f 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -4,7 +4,7 @@ backend in order to ensure that the similarity methods only require to match this interface. """ -from typing import Callable, Union, Optional, List +from typing import Callable, Union, Optional import numpy as np import torch.nn as nn @@ -105,19 +105,18 @@ def argmax(X: torch.Tensor, dim=-1) -> torch.Tensor: return torch.argmax(X, dim=dim) @staticmethod - def _get_non_trainable(model: nn.Module) -> List[Optional[str]]: - """Returns a list of non trainable parameters. + def _count_non_trainable(model: nn.Module) -> int: + """Returns number of non trainable parameters. - Returns a list of names of parameters that are non trainable. If no trainable parameter exists we raise + Returns the number of parameters that are non trainable. If no trainable parameter exists we raise a `ValueError`. """ - params = [name if name else None for name, param in model.named_parameters() - if not param.requires_grad] + num_non_trainable_params = len([param for param in model.parameters() if not param.requires_grad]) - if len(params) == len(list(model.parameters())): + if num_non_trainable_params == len(list(model.parameters())): raise ValueError("The model has no trainable parameters. This method requires at least " "one trainable parameter to compute the gradients for. " "Try setting ``.requires_grad_(True)`` on the model or one of its parameters.") - return params + return num_non_trainable_params diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index 9fd139595..56316045e 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -4,7 +4,7 @@ backend in order to ensure that the similarity methods only require to match this interface. """ -from typing import Callable, Optional, Union, List +from typing import Callable, Optional, Union import numpy as np import tensorflow as tf @@ -101,10 +101,10 @@ def argmax(X: tf.Tensor, dim=-1) -> tf.Tensor: return X @staticmethod - def _get_non_trainable(model: keras.Model) -> List[Optional[str]]: - """Returns a list of non trainable parameters. + def _count_non_trainable(model: keras.Model) -> int: + """Returns number of non trainable parameters. - Returns a list of names of parameters that are non trainable. If no trainable parameter exists we raise + Returns the number of parameters that are non trainable. If no trainable parameter exists we raise a `ValueError`. """ @@ -112,4 +112,4 @@ def _get_non_trainable(model: keras.Model) -> List[Optional[str]]: raise ValueError("The model has no trainable weights. This method requires at least " "one trainable parameter to compute the gradients for. " "Set ``trainable=True`` on the model or a model weight.") - return [getattr(weight, 'name', None) for weight in model.non_trainable_weights] + return len(model.non_trainable_weights) diff --git a/alibi/explainers/similarity/grad.py b/alibi/explainers/similarity/grad.py index 2b5707c51..a04f64a26 100644 --- a/alibi/explainers/similarity/grad.py +++ b/alibi/explainers/similarity/grad.py @@ -129,14 +129,15 @@ def __init__(self, task_name=task ) - non_trainable_layers = self.backend._get_non_trainable(self.predictor) - if non_trainable_layers: - layers_msg = 'The following tensors are non-trainable: ' - layers = ", ".join([f"'{layer}'" for layer in non_trainable_layers if layer is not None]) - warning_msg = ("Some layers in the model are non-trainable. These layers don't have gradients " - "and will not be included in the computation of gradient similarity. " - f"{layers_msg}{layers}") - warnings.warn(warning_msg) # todo: scope warning to this location + num_non_trainable = self.backend._count_non_trainable(self.predictor) + if num_non_trainable: + warning_msg = (f"Found {num_non_trainable} non-trainable parameters in the model. These parameters " + "don't have gradients and will not be included in the computation of gradient similarity." + " This might be because your model has layers that track statistics using non-trainable " + "parameters such as batch normalization layers. In this case, you don't need to worry. " + "Otherwise it's because you have set some parameters to be non-trainable and alibi is " + "letting you know.") + warnings.warn(warning_msg) def fit(self, X_train: np.ndarray, diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index e5b1cb7ae..e66630f21 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -363,10 +363,10 @@ def test_non_trainable_layer_warnings_tf(): model.layers[1].trainable = False model.layers[-1].layers[1].trainable = False - tensor_names = [tensor.name for tensor in model.layers[1].non_trainable_weights] - tensor_names.extend([tensor.name for tensor in model.layers[-1].layers[1].non_trainable_weights]) - tensor_names.extend([tensor.name for tensor in model.layers[2].non_trainable_weights]) - + # tensor_names = [tensor.name for tensor in model.layers[1].non_trainable_weights] + # tensor_names.extend([tensor.name for tensor in model.layers[-1].layers[1].non_trainable_weights]) + # tensor_names.extend([tensor.name for tensor in model.layers[2].non_trainable_weights]) + num_params_non_trainable = len(model.non_trainable_weights) loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) with pytest.warns(Warning) as record: @@ -377,15 +377,12 @@ def test_non_trainable_layer_warnings_tf(): backend='tensorflow', ) - assert ("Some layers in the model are non-trainable. These layers don't have " - "gradients and will not be included in the computation of gradient similarity.") \ - in str(record[0].message) - - assert "The following tensors are non-trainable:" \ - in str(record[0].message) - for tensor_name in tensor_names: - # print(tensor_name, str(record[0].message).split(':')[-1]) - assert tensor_name in str(record[0].message) + assert (f"Found {num_params_non_trainable} non-trainable parameters in the model. These parameters " + "don't have gradients and will not be included in the computation of gradient similarity." + " This might be because your model has layers that track statistics using non-trainable " + "parameters such as batch normalization layers. In this case, you don't need to worry. " + "Otherwise it's because you have set some parameters to be non-trainable and alibi is " + "letting you know.") == str(record[0].message) def test_non_trainable_layer_warnings_pt(): @@ -417,7 +414,7 @@ def forward(self, x): model = Model(10, 20) model.linear_stack[2].weight.requires_grad = False model.linear_stack[4][1].weight.requires_grad = False - tensor_names = [name for name, tensor in model.named_parameters() if not tensor.requires_grad] + num_params_non_trainable = len([param for param in model.parameters() if not param.requires_grad]) loss_fn = nn.CrossEntropyLoss() @@ -429,14 +426,12 @@ def forward(self, x): backend='pytorch' ) - assert ("Some layers in the model are non-trainable. These layers don't have " - "gradients and will not be included in the computation of gradient similarity.") \ - in str(record[0].message) - - assert "The following tensors are non-trainable:" \ - in str(record[0].message) - for tensor_name in tensor_names: - assert tensor_name in str(record[0].message) + assert (f"Found {num_params_non_trainable} non-trainable parameters in the model. These parameters " + "don't have gradients and will not be included in the computation of gradient similarity." + " This might be because your model has layers that track statistics using non-trainable " + "parameters such as batch normalization layers. In this case, you don't need to worry. " + "Otherwise it's because you have set some parameters to be non-trainable and alibi is " + "letting you know.") == str(record[0].message) def test_not_trainable_model_error_tf(): From f72d1972b84b4f132956ee249b7d0ab76356c312 Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Tue, 10 Jan 2023 13:16:41 +0000 Subject: [PATCH 33/35] Minor fix --- alibi/explainers/similarity/grad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alibi/explainers/similarity/grad.py b/alibi/explainers/similarity/grad.py index a04f64a26..2b75dfe10 100644 --- a/alibi/explainers/similarity/grad.py +++ b/alibi/explainers/similarity/grad.py @@ -145,7 +145,7 @@ def fit(self, """Fit the explainer. The `GradientSimilarity` explainer requires the model gradients over the training data. In the explain method - it compares them to the model gradients for the test instance(s). If `precompute_grads` was set to ``True`` on + it compares them to the model gradients for the test instance(s). If ``precompute_grads=True`` on initialization then the gradients are precomputed here and stored. This will speed up the explain method call but storing the gradients may not be feasible for large models. From 308b51fca71cac9c3767af468fc4579b17e4456f Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Tue, 10 Jan 2023 13:30:21 +0000 Subject: [PATCH 34/35] Remove backticks --- alibi/explainers/similarity/backends/pytorch/base.py | 2 +- alibi/explainers/similarity/backends/tensorflow/base.py | 2 +- .../tests/test_simiarlity/test_grad_methods_unit.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/alibi/explainers/similarity/backends/pytorch/base.py b/alibi/explainers/similarity/backends/pytorch/base.py index 50f45d34f..1bac78199 100644 --- a/alibi/explainers/similarity/backends/pytorch/base.py +++ b/alibi/explainers/similarity/backends/pytorch/base.py @@ -91,7 +91,7 @@ def set_device(device: Union[str, int, torch.device, None] = None) -> None: elif isinstance(device, torch.device): _PytorchBackend.device = device elif device is not None: - raise TypeError(("`device` must be a ``None``, ``string``, ``integer`` or " + raise TypeError(("`device` must be a ``None``, `string`, `integer` or " f"`torch.device` object. Got {type(device)} instead.")) @staticmethod diff --git a/alibi/explainers/similarity/backends/tensorflow/base.py b/alibi/explainers/similarity/backends/tensorflow/base.py index 56316045e..1305834f1 100644 --- a/alibi/explainers/similarity/backends/tensorflow/base.py +++ b/alibi/explainers/similarity/backends/tensorflow/base.py @@ -87,7 +87,7 @@ def set_device(device: Union[str, None] = None) -> None: if device is None or isinstance(device, str): _TensorFlowBackend.device = device else: - raise TypeError(f"`device` must be a ``string`` or ``None``. Got {type(device)} instead.") + raise TypeError(f"`device` must be a `string` or ``None``. Got {type(device)} instead.") @staticmethod def to_numpy(X: tf.Tensor) -> np.ndarray: diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py index 571c7daa6..cc431b208 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_unit.py @@ -233,7 +233,7 @@ def test_device_error_msgs(linear_reg_model): device=[0] ) if backend == 'pytorch': - assert ("`device` must be a ``None``, ``string``, ``integer`` or `torch.device` object." + assert ("`device` must be a ``None``, `string`, `integer` or `torch.device` object." " Got instead.") in str(err.value) elif backend == 'tensorflow': - assert "`device` must be a ``string`` or ``None``. Got instead." in str(err.value) + assert "`device` must be a `string` or ``None``. Got instead." in str(err.value) From 4bd1f2013fdf4e54165cd57c36ff12e01c1a4dfc Mon Sep 17 00:00:00 2001 From: Alex Athorne Date: Tue, 10 Jan 2023 15:45:46 +0000 Subject: [PATCH 35/35] Remove commented out lines from test --- .../tests/test_simiarlity/test_grad_methods_integration.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py index e66630f21..d59e0535c 100644 --- a/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py +++ b/alibi/explainers/tests/test_simiarlity/test_grad_methods_integration.py @@ -362,10 +362,6 @@ def test_non_trainable_layer_warnings_tf(): model.layers[1].trainable = False model.layers[-1].layers[1].trainable = False - - # tensor_names = [tensor.name for tensor in model.layers[1].non_trainable_weights] - # tensor_names.extend([tensor.name for tensor in model.layers[-1].layers[1].non_trainable_weights]) - # tensor_names.extend([tensor.name for tensor in model.layers[2].non_trainable_weights]) num_params_non_trainable = len(model.non_trainable_weights) loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)