From 2b416b75687520f5b0b8185b6946bae9a733ae69 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Tue, 2 May 2023 18:14:12 +0200 Subject: [PATCH 01/26] first_order: fix instantiation of the ihvp when using a string as an argument --- deel/influenciae/common/model_wrappers.py | 2 +- deel/influenciae/influence/__init__.py | 1 + .../influence/base_group_influence.py | 7 ++- tests/common/test_inf_abstract.py | 57 ++++++++++++++++++- 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/deel/influenciae/common/model_wrappers.py b/deel/influenciae/common/model_wrappers.py index 7c3b60e..fd0931e 100644 --- a/deel/influenciae/common/model_wrappers.py +++ b/deel/influenciae/common/model_wrappers.py @@ -379,7 +379,7 @@ def __init__(self, loss_function: Callable = tf.keras.losses.CategoricalCrossentropy( from_logits=False, reduction=Reduction.NONE), process_batch_for_loss_fn: ProcessBatchTypeAlias = default_process_batch): - + self.start_layer = start_layer weights_to_watch = InfluenceModel._get_weights_of_interest(model, start_layer, last_layer) super().__init__(model, weights_to_watch, loss_function, process_batch_for_loss_fn, weights_processed=True) diff --git a/deel/influenciae/influence/__init__.py b/deel/influenciae/influence/__init__.py index 30c64df..6cb4ab5 100644 --- a/deel/influenciae/influence/__init__.py +++ b/deel/influenciae/influence/__init__.py @@ -8,3 +8,4 @@ from .first_order_influence_calculator import FirstOrderInfluenceCalculator from .second_order_influence_calculator import SecondOrderInfluenceCalculator from .arnoldi_influence_calculator import ArnoldiInfluenceCalculator +from .base_group_influence import BaseGroupInfluenceCalculator diff --git a/deel/influenciae/influence/base_group_influence.py b/deel/influenciae/influence/base_group_influence.py index 6dbc69b..79ea407 100644 --- a/deel/influenciae/influence/base_group_influence.py +++ b/deel/influenciae/influence/base_group_influence.py @@ -72,8 +72,11 @@ def __init__( # load ivhp calculator from str, IHVPcalculator enum or InverseHessianVectorProduct object if isinstance(ihvp_calculator, str): - self.ihvp_calculator = IHVPCalculator.from_string(ihvp_calculator).value(self.model, - self.train_set) + self.ihvp_calculator = IHVPCalculator.from_string(ihvp_calculator).value(self.model, self.train_set) if \ + ihvp_calculator == 'exact' else \ + IHVPCalculator.from_string(ihvp_calculator).value(self.model, + self.model.start_layer, + self.train_set) elif isinstance(ihvp_calculator, IHVPCalculator): self.ihvp_calculator = ihvp_calculator.value(self.model, self.train_set) elif isinstance(ihvp_calculator, InverseHessianVectorProduct): diff --git a/tests/common/test_inf_abstract.py b/tests/common/test_inf_abstract.py index c8dd484..a41f206 100644 --- a/tests/common/test_inf_abstract.py +++ b/tests/common/test_inf_abstract.py @@ -2,4 +2,59 @@ # rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, # CRIAQ and ANITI - https://www.deel.ai/ # ===================================================================================== -# TODO: for now the test is included in first order +import tensorflow as tf +from tensorflow.keras.layers import Input, Dense +from tensorflow.keras.models import Sequential +from tensorflow.keras.losses import Reduction, MeanSquaredError + +from deel.influenciae.common import InfluenceModel +from deel.influenciae.common import ExactIHVP, ConjugateGradientDescentIHVP, LissaIHVP +from deel.influenciae.influence import FirstOrderInfluenceCalculator + + +def test_instantiation(): + """ + Test that the instantiation happens as it should + """ + # start with a simple model + model = Sequential([Input(shape=(1, 3)), Dense(2, use_bias=False), Dense(1, use_bias=False)]) + model.build(input_shape=(1, 3)) + + # build the influence model + influence_model = InfluenceModel(model, start_layer=-1, loss_function=MeanSquaredError(reduction=Reduction.NONE)) + + # build a fake dataset in order to have batched samples + inputs = tf.random.normal((25, 1, 3)) + target = tf.random.normal((25, 1)) + train_set = tf.data.Dataset.from_tensor_slices((inputs, target)) + + # Test several configurations + ihvp_objects = [ExactIHVP(influence_model, train_set.batch(5)), + ConjugateGradientDescentIHVP(influence_model, -1, train_set.batch(5)), + LissaIHVP(influence_model, -1, train_set.batch(5))] + ihvp_strings = ["exact", "cgd", "lissa"] + ihvp_classes = [ExactIHVP, ConjugateGradientDescentIHVP, LissaIHVP] + normalization = [True, False] + + for ihvp_calculator in ihvp_objects: + for normalize in normalization: + FirstOrderInfluenceCalculator( + influence_model, + train_set.batch(5), + ihvp_calculator, + n_samples_for_hessian=25, + shuffle_buffer_size=25, + normalize=normalize + ) + + for ihvp_string, classes in zip(ihvp_strings, ihvp_classes): + for normalize in normalization: + influence_calculator = FirstOrderInfluenceCalculator( + influence_model, + train_set.batch(5), + ihvp_string, + n_samples_for_hessian=25, + shuffle_buffer_size=25, + normalize=normalize + ) + assert isinstance(influence_calculator.ihvp_calculator, classes) From fa2d68b6ee554daee6e773a8990a50dd6f9c9996 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Tue, 2 May 2023 18:45:34 +0200 Subject: [PATCH 02/26] second_order: fix estimation of the self-influence values --- .../influence/second_order_influence_calculator.py | 2 ++ tests/influence/test_second_order_influence.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/deel/influenciae/influence/second_order_influence_calculator.py b/deel/influenciae/influence/second_order_influence_calculator.py index 66514eb..6e6d673 100644 --- a/deel/influenciae/influence/second_order_influence_calculator.py +++ b/deel/influenciae/influence/second_order_influence_calculator.py @@ -207,6 +207,8 @@ def estimate_influence_values_group( influence_values_group A tensor containing one influence value for the whole group. """ + if group_to_evaluate is None: + group_to_evaluate = group_train ds_size = self.assert_compatible_datasets(group_train, group_to_evaluate) influence = tf.transpose(self.compute_influence_vector_group(group_train)) reduced_grads = tf.reduce_sum(tf.reshape(self.model.batch_jacobian(group_to_evaluate), diff --git a/tests/influence/test_second_order_influence.py b/tests/influence/test_second_order_influence.py index f75457d..f6ee565 100644 --- a/tests/influence/test_second_order_influence.py +++ b/tests/influence/test_second_order_influence.py @@ -193,6 +193,8 @@ def test_compute_influence_values_group(): reduced_ground_truth_grads_test = tf.reduce_sum(ground_truth_grads_test, axis=1, keepdims=True) ground_truth_influence_values = tf.matmul(reduced_ground_truth_grads_test, ground_truth_influence_group, transpose_a=True, transpose_b=True) + ground_truth_self_influence = tf.matmul(reduced_ground_truth_grads, ground_truth_influence_group, + transpose_a=True, transpose_b=True) # Check if the result is the one expected calculators = [ @@ -209,6 +211,10 @@ def test_compute_influence_values_group(): assert influence_group_values.shape == (1, 1) assert almost_equal(influence_group_values, ground_truth_influence_values, epsilon=1e-3) + self_influence_group = influence_calculator.estimate_influence_values_group(train_set.take(5).batch(5)) + assert self_influence_group.shape == (1, 1) + assert almost_equal(self_influence_group, ground_truth_self_influence, epsilon=1e-3) + def test_cnn_shapes(): """ From 0a0f0327282ec51012eb0d10f178f13f48fb920a Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Wed, 26 Jul 2023 20:51:13 +0200 Subject: [PATCH 03/26] boundary: fix mangling warning on tf --- deel/influenciae/boundary_based/sample_boundary.py | 4 ++-- deel/influenciae/boundary_based/weights_boundary.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deel/influenciae/boundary_based/sample_boundary.py b/deel/influenciae/boundary_based/sample_boundary.py index f72a42d..4ef0b13 100644 --- a/deel/influenciae/boundary_based/sample_boundary.py +++ b/deel/influenciae/boundary_based/sample_boundary.py @@ -75,7 +75,7 @@ def __delta_to_index(indexes_1: tf.Tensor, indexes_2: tf.Tensor, x: tf.Tensor): return delta_x @tf.function - def __step(self, x: tf.Tensor, y_pred: tf.Tensor) -> Tuple[tf.Tensor, tf.Tensor, tf.Tensor]: + def _step(self, x: tf.Tensor, y_pred: tf.Tensor) -> Tuple[tf.Tensor, tf.Tensor, tf.Tensor]: """ The optimization step to find the distance between the boundary and a given sample x. @@ -163,7 +163,7 @@ def __compute_single_sample_score(self, x: tf.Tensor) -> tf.Tensor: y_pred = self.model(x) def body(index, x_current): - computation, _, x_new = self.__step(x_current, y_pred) + computation, _, x_new = self._step(x_current, y_pred) return computation, index + 1, x_new _, _, x_adversarial = tf.while_loop( diff --git a/deel/influenciae/boundary_based/weights_boundary.py b/deel/influenciae/boundary_based/weights_boundary.py index c569e8b..72dcf6b 100644 --- a/deel/influenciae/boundary_based/weights_boundary.py +++ b/deel/influenciae/boundary_based/weights_boundary.py @@ -98,7 +98,7 @@ def __delta_to_index(self, indexes_1: tf.Tensor, indexes_2: tf.Tensor, x: tf.Ten return delta_x @tf.function - def __step(self, x: tf.Tensor, y_pred: tf.Tensor) -> Tuple[tf.Tensor, tf.Tensor]: + def _step(self, x: tf.Tensor, y_pred: tf.Tensor) -> Tuple[tf.Tensor, tf.Tensor]: """ The optimization step to find the distance between the boundary and a given sample x. To see more details about the optimization procedure for multi-class classifiers, @@ -211,7 +211,7 @@ def __compute_single_sample_score(self, x: tf.Tensor) -> tf.Tensor: y_pred = self.model(x) tf.while_loop(lambda cond, index: tf.logical_and(cond, index < self.step_nbr), - lambda cond, index: (self.__step(x, y_pred)[0], index + 1), + lambda cond, index: (self._step(x, y_pred)[0], index + 1), [tf.constant(True), tf.constant(0, dtype=tf.int32)]) score = self.__delta_weights() From a497fd9a3d4000b4aa817d2f7c90c2ee16de0cd0 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Wed, 30 Aug 2023 16:36:41 +0200 Subject: [PATCH 04/26] bench: add factories for new methods to init --- deel/influenciae/benchmark/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/deel/influenciae/benchmark/__init__.py b/deel/influenciae/benchmark/__init__.py index 1f0c2b9..3dbe969 100644 --- a/deel/influenciae/benchmark/__init__.py +++ b/deel/influenciae/benchmark/__init__.py @@ -7,5 +7,13 @@ """ from .base_benchmark import BaseTrainingProcedure, MislabelingDetectorEvaluator -from .influence_factory import InfluenceCalculatorFactory, FirstOrderFactory, RPSLJEFactory, TracInFactory +from .influence_factory import ( + InfluenceCalculatorFactory, + FirstOrderFactory, + RPSLJEFactory, + TracInFactory, + WeightsBoundaryCalculatorFactory, + SampleBoundaryCalculatorFactory, + ArnoldiCalculatorFactory +) from .cifar10_benchmark import Cifar10TrainingProcedure, Cifar10MislabelingDetectorEvaluator From bacd5cc53d6663cf1c247fa61c2e189c22d766a4 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Wed, 30 Aug 2023 16:38:17 +0200 Subject: [PATCH 05/26] doc: add a doc page to the new influence calculators --- docs/api/Boundary-based/sample_boundary.md | 25 ++++++++++++++++++ docs/api/Boundary-based/weights_boundary.md | 25 ++++++++++++++++++ docs/api/influence/arnoldi.md | 28 +++++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 docs/api/Boundary-based/sample_boundary.md create mode 100644 docs/api/Boundary-based/weights_boundary.md create mode 100644 docs/api/influence/arnoldi.md diff --git a/docs/api/Boundary-based/sample_boundary.md b/docs/api/Boundary-based/sample_boundary.md new file mode 100644 index 0000000..56760f0 --- /dev/null +++ b/docs/api/Boundary-based/sample_boundary.md @@ -0,0 +1,25 @@ +# Sample boundary + + +[View source](https://github.com/deel-ai/influenciae/blob/main/deel/influenciae/boundary_based/sample_boundary.py) + +For a completely different notion of influence or importance of data-points, we propose to measure the distance that +separates each data-point from the decision boundary, and assign a higher influence score to the elements that are +closest to the decision boundary. It would make sense for these examples to be the most influential, as if they weren't +there, the model would have placed the decision boundary elsewhere. + +In particular, we define the influence score as follows: + +$$ \mathcal{I}_SB (z) = - \lVert z - z_{adv} \rVert^2 \, , $$ +where $z$ is the data-point under study and $z_{adv}$ is the adversarial example with the lowest possible budget +and obtained through the [DeepFool method](https://arxiv.org/abs/1511.04599). + +This technique is based on a simple idea we had, and as such, there's no paper associated to it. We decided to include +it because it seems that its performance is less dependent on the choice of model and training schedule and still +obtains acceptable results on our mislabeled point detection benchmark. + +## Notebooks + +- [**Using Boundary-based Influence**](https://drive.google.com/file/d/145Gi4gCYTKlRVJjsty5cPkdMGNJoNDws/view?usp=share_link) + +{{deel.influenciae.boundary_based.sample_boundary.SampleBoundaryCalculator}} diff --git a/docs/api/Boundary-based/weights_boundary.md b/docs/api/Boundary-based/weights_boundary.md new file mode 100644 index 0000000..091fc22 --- /dev/null +++ b/docs/api/Boundary-based/weights_boundary.md @@ -0,0 +1,25 @@ +# Weights boundary + + +[View source](https://github.com/deel-ai/influenciae/blob/main/deel/influenciae/boundary_based/weights_boundary.py) + +For a completely different notion of influence or importance of data-points, we propose to measure the budget (measured +through an $\el^2$ metric) needed to minimally perturb the model's weights such that the data-point under study gets +misclassified. Ideally, it would make sense for more influential images to need a smaller budget (i.e. a smaller change +on the model) to make the model change its prediction on them. + +In particular, we define the influence score as follows: + +$$ \mathcal{I}_SB (z) = - \lVert w - w_{adv} \rVert^2 \, , $$ +where $w$ is the model's weights and $w_{adv}$ is the perturbed model with the lowest possible budget and +obtained through an adaptation of the [DeepFool method](https://arxiv.org/abs/1511.04599). + +This technique is based on a simple idea we had, and as such, there's no paper associated to it. We decided to include +it because it seems that its performance is less dependent on the choice of model and training schedule and still +obtains acceptable results on our mislabeled point detection benchmark. + +## Notebooks + +- [**Using Boundary-based Influence**](https://drive.google.com/file/d/145Gi4gCYTKlRVJjsty5cPkdMGNJoNDws/view?usp=share_link) + +{{deel.influenciae.boundary_based.sample_boundary.WeightsBoundaryCalculator}} diff --git a/docs/api/influence/arnoldi.md b/docs/api/influence/arnoldi.md new file mode 100644 index 0000000..bb578ac --- /dev/null +++ b/docs/api/influence/arnoldi.md @@ -0,0 +1,28 @@ +# Arnoldi Influence Calculator + + + +[View source](https://github.com/deel-ai/influenciae/blob/main/deel/influenciae/influence/arnoldi_influence_calculator.py) | +📰 [Paper](https://arxiv.org/abs/2112.03052) + +This class implements the method introduced in *Scaling Up Influence Functions, Schioppa et al.* at AAAI 2022. +It proposes a series of memory and computational optimizations based on the Arnoldi iteration for speeding up +inverse hessian calculators, allowing the authors to approximately compute influence functions on whole +large vision models (going up to a ViT-L with 300M parameters). + +In essence, the optimizations can be summarized as follows: +- build an orthonormal basis for the Krylov subspaces of a random vector (in the desired dimensionality). +- find the eigenvalues and eigenvectors of the restriction of the Hessian matrix in that restricted subspace. +- keep only the $k$ largest eigenvalues and their corresponding eigenvectors, and create a projection matrix $G$ into this space. +- use forward-over-backward auto-differentiation to directly compute the JVPs in this reduced space. + +Due to the specificity of these optimizations, the inverse hessian vector product operation is implemented inside the +class, and thus, doesn't require an additional separate IHVP object. In addition, it can only be applied to individual +points for the moment. + + +## Notebooks + +- [**Using Arnoldi Influence Calculator**](https://colab.research.google.com/drive/1rQU33sbD0YW1cZMRlJmS15EW5O16yoDE?usp=sharing) + +{{deel.influenciae.influence.first_order_influence_calculator.ArnoldiInfluenceCalculator}} From be4f7eaa0c3efd322a588ab3d908bfea01aea16f Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Mon, 2 Oct 2023 16:46:27 +0200 Subject: [PATCH 06/26] doc: fix the equations in doc for boundary based calculators --- docs/api/Boundary-based/sample_boundary.md | 2 +- docs/api/Boundary-based/weights_boundary.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api/Boundary-based/sample_boundary.md b/docs/api/Boundary-based/sample_boundary.md index 56760f0..a7bf127 100644 --- a/docs/api/Boundary-based/sample_boundary.md +++ b/docs/api/Boundary-based/sample_boundary.md @@ -10,7 +10,7 @@ there, the model would have placed the decision boundary elsewhere. In particular, we define the influence score as follows: -$$ \mathcal{I}_SB (z) = - \lVert z - z_{adv} \rVert^2 \, , $$ +$$ \mathcal{I}_{SB} (z) = - \lVert z - z_{adv} \rVert^2 \, , $$ where $z$ is the data-point under study and $z_{adv}$ is the adversarial example with the lowest possible budget and obtained through the [DeepFool method](https://arxiv.org/abs/1511.04599). diff --git a/docs/api/Boundary-based/weights_boundary.md b/docs/api/Boundary-based/weights_boundary.md index 091fc22..f5388da 100644 --- a/docs/api/Boundary-based/weights_boundary.md +++ b/docs/api/Boundary-based/weights_boundary.md @@ -10,7 +10,7 @@ on the model) to make the model change its prediction on them. In particular, we define the influence score as follows: -$$ \mathcal{I}_SB (z) = - \lVert w - w_{adv} \rVert^2 \, , $$ +$$ \mathcal{I}_{WB} (z) = - \lVert w - w_{adv} \rVert^2 \, , $$ where $w$ is the model's weights and $w_{adv}$ is the perturbed model with the lowest possible budget and obtained through an adaptation of the [DeepFool method](https://arxiv.org/abs/1511.04599). From 1bd433fb8be16880d12810c67b0eb83cf59f9fb0 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Thu, 23 Nov 2023 18:16:39 +0100 Subject: [PATCH 07/26] doc: update the readme and documentation with the latest tutorials --- README.md | 16 ++++++++------- docs/api/Boundary-based/sample_boundary.md | 2 +- docs/api/Boundary-based/weights_boundary.md | 2 +- docs/index.md | 22 ++++++++++++--------- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9805dda..f35ac7f 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,11 @@ We propose some hands-on tutorials to get familiar with the library and it's API - [**Benchmarking with Mislabeled sample detection**](https://colab.research.google.com/drive/1_5-RC_YBHptVCElBbjxWfWQ1LMU20vOp?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1_5-RC_YBHptVCElBbjxWfWQ1LMU20vOp?usp=sharing) - [**Using the first order influence calculator**](https://colab.research.google.com/drive/1WlYcQNu5obhVjhonN2QYi8ybKyZJl4iY?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1WlYcQNu5obhVjhonN2QYi8ybKyZJl4iY?usp=sharing) - [**Using the second order influence calculator**](https://colab.research.google.com/drive/1qNvKiU3-aZWhRA0rxS6X3ebeNkoznJJe?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1qNvKiU3-aZWhRA0rxS6X3ebeNkoznJJe?usp=sharing) +- [**Using Arnoldi Influence Calculator**](https://colab.research.google.com/drive/1rQU33sbD0YW1cZMRlJmS15EW5O16yoDE?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1rQU33sbD0YW1cZMRlJmS15EW5O16yoDE?usp=sharing) - [**Using TracIn**](https://colab.research.google.com/drive/1E94cGF46SUQXcCTNwQ4VGSjXEKm7g21c?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1E94cGF46SUQXcCTNwQ4VGSjXEKm7g21c?usp=sharing) - [**Using Representer Point Selection - L2 (RPS_L2)**](https://colab.research.google.com/drive/17W5s30LbxABbDd8hbdwYE56abyWjSC4u?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/17W5s30LbxABbDd8hbdwYE56abyWjSC4u?usp=sharing) - [**Using Representer Point Selection - Local Jacobian Expansion (RPS_LJE)**](https://colab.research.google.com/drive/14e7wwFRQJhY-huVYmJ7ri355kfLJgAPA?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/14e7wwFRQJhY-huVYmJ7ri355kfLJgAPA?usp=sharing) +- [**Using Boundary-based Influence**](https://colab.research.google.com/drive/1785eHgT91FfqG1f25s7ovqd6JhP5uklh?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1785eHgT91FfqG1f25s7ovqd6JhP5uklh?usp=sharing) ## 🚀 Quick Start @@ -63,7 +65,7 @@ from deel.influenciae.utils import ORDER # load the model, the training loss (without reduction) and the training data (with the labels and in a batched TF dataset) -influence_model = InfluenceModel(model, target_layer, loss_function) +influence_model = InfluenceModel(model, start_layer=target_layer, loss_function=loss_function) ihvp_calculator = ExactIHVP(influence_model, train_dataset) influence_calculator = FirstOrderInfluenceCalculator(influence_model, train_dataset, ihvp_calculator) data_and_influence_dataset = influence_calculator.compute_influence_values(train_dataset) @@ -85,7 +87,7 @@ from deel.influenciae.utils import ORDER # load the model, the training loss (without reduction), the training data and # the data to explain (with the labels and in batched a TF dataset) -influence_model = InfluenceModel(model, target_layer, loss_function) +influence_model = InfluenceModel(model, start_layer=target_layer, loss_function=loss_function) ihvp_calculator = ExactIHVP(influence_model, train_dataset) influence_calculator = FirstOrderInfluenceCalculator(influence_model, train_dataset, ihvp_calculator) data_and_influence_dataset = influence_calculator.estimate_influence_values_in_batches(samples_to_explain, train_dataset) @@ -108,7 +110,7 @@ from deel.influenciae.influence import SecondOrderInfluenceCalculator # load the model, the training loss (without reduction), the training data and # the data to explain (with the labels and in a batched TF dataset) -influence_model = InfluenceModel(model, target_layer, loss_function) +influence_model = InfluenceModel(model, start_layer=target_layer, loss_function=loss_function) ihvp_calculator = ExactIHVP(influence_model, train_dataset) influence_calculator = SecondOrderInfluenceCalculator(influence_model, train_dataset, ihvp_calculator) # or FirstOrderInfluenceCalculator data_and_influence_dataset = influence_calculator.estimate_influence_values_group(groups_train, groups_to_explain) @@ -123,7 +125,7 @@ from deel.influenciae.influence import SecondOrderInfluenceCalculator # load the model, the training loss (without reduction), the training data and # the data to explain (with the labels and in a batched TF dataset) -influence_model = InfluenceModel(model, target_layer, loss_function) +influence_model = InfluenceModel(model, start_layer=target_layer, loss_function=loss_function) ihvp_calculator = ExactIHVP(influence_model, train_dataset) influence_calculator = SecondOrderInfluenceCalculator(influence_model, train_dataset, ihvp_calculator) # or FirstOrderInfluenceCalculator data_and_influence_dataset = influence_calculator.estimate_influence_values_group(groups_train) @@ -139,11 +141,11 @@ All the influence calculation methods work on Tensorflow models trained for any | RelatIF | [Paper](https://arxiv.org/pdf/2003.11630.pdf) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1WlYcQNu5obhVjhonN2QYi8ybKyZJl4iY?usp=sharing) | | Influence Functions (first order, groups) | [Paper](https://arxiv.org/abs/1905.13289) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1WlYcQNu5obhVjhonN2QYi8ybKyZJl4iY?usp=sharing) | | Influence Functions (second order, groups) | [Paper](https://arxiv.org/abs/1911.00418) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1qNvKiU3-aZWhRA0rxS6X3ebeNkoznJJe?usp=sharing) | -| Arnoldi (Scaling Up Influence Functions) | [Paper](https://arxiv.org/abs/2112.03052) | WIP | +| Arnoldi iteration (Scaling Up Influence Functions) | [Paper](https://arxiv.org/abs/2112.03052) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1rQU33sbD0YW1cZMRlJmS15EW5O16yoDE?usp=sharing) | +| Trac-In | [Paper](https://arxiv.org/abs/2002.08484) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1E94cGF46SUQXcCTNwQ4VGSjXEKm7g21c?usp=sharing) | | Representer Point Selection (L2) | [Paper](https://arxiv.org/abs/1811.09720) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/17W5s30LbxABbDd8hbdwYE56abyWjSC4u?usp=sharing) | | Representer Point Selection (Local Jacobian Expansion) | [Paper](https://proceedings.neurips.cc/paper/2021/file/c460dc0f18fc309ac07306a4a55d2fd6-Paper.pdf) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/14e7wwFRQJhY-huVYmJ7ri355kfLJgAPA?usp=sharing) | -| Trac-In | [Paper](https://arxiv.org/abs/2002.08484) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1E94cGF46SUQXcCTNwQ4VGSjXEKm7g21c?usp=sharing) | -| Boundary-based influence | -- | WIP | +| Boundary-based influence | -- | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1785eHgT91FfqG1f25s7ovqd6JhP5uklh?usp=sharing) | ## 👀 See Also diff --git a/docs/api/Boundary-based/sample_boundary.md b/docs/api/Boundary-based/sample_boundary.md index a7bf127..853eef0 100644 --- a/docs/api/Boundary-based/sample_boundary.md +++ b/docs/api/Boundary-based/sample_boundary.md @@ -20,6 +20,6 @@ obtains acceptable results on our mislabeled point detection benchmark. ## Notebooks -- [**Using Boundary-based Influence**](https://drive.google.com/file/d/145Gi4gCYTKlRVJjsty5cPkdMGNJoNDws/view?usp=share_link) +- [**Using Boundary-based Influence**](https://colab.research.google.com/drive/1785eHgT91FfqG1f25s7ovqd6JhP5uklh?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1785eHgT91FfqG1f25s7ovqd6JhP5uklh?usp=sharing) {{deel.influenciae.boundary_based.sample_boundary.SampleBoundaryCalculator}} diff --git a/docs/api/Boundary-based/weights_boundary.md b/docs/api/Boundary-based/weights_boundary.md index f5388da..62a3b37 100644 --- a/docs/api/Boundary-based/weights_boundary.md +++ b/docs/api/Boundary-based/weights_boundary.md @@ -20,6 +20,6 @@ obtains acceptable results on our mislabeled point detection benchmark. ## Notebooks -- [**Using Boundary-based Influence**](https://drive.google.com/file/d/145Gi4gCYTKlRVJjsty5cPkdMGNJoNDws/view?usp=share_link) +- [**Using Boundary-based Influence**](https://colab.research.google.com/drive/1785eHgT91FfqG1f25s7ovqd6JhP5uklh?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1785eHgT91FfqG1f25s7ovqd6JhP5uklh?usp=sharing) {{deel.influenciae.boundary_based.sample_boundary.WeightsBoundaryCalculator}} diff --git a/docs/index.md b/docs/index.md index 1497058..8b78804 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,6 +40,8 @@ We propose some hands-on tutorials to get familiar with the library and it's API - [**Using TracIn**](https://colab.research.google.com/drive/1E94cGF46SUQXcCTNwQ4VGSjXEKm7g21c?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1E94cGF46SUQXcCTNwQ4VGSjXEKm7g21c?usp=sharing) - [**Using Representer Point Selection - L2 (RPS_L2)**](https://colab.research.google.com/drive/17W5s30LbxABbDd8hbdwYE56abyWjSC4u?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/17W5s30LbxABbDd8hbdwYE56abyWjSC4u?usp=sharing) - [**Using Representer Point Selection - Local Jacobian Expansion (RPS_LJE)**](https://colab.research.google.com/drive/14e7wwFRQJhY-huVYmJ7ri355kfLJgAPA?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/14e7wwFRQJhY-huVYmJ7ri355kfLJgAPA?usp=sharing) +- [**Using Arnoldi Influence Calculator**](https://colab.research.google.com/drive/1rQU33sbD0YW1cZMRlJmS15EW5O16yoDE?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1rQU33sbD0YW1cZMRlJmS15EW5O16yoDE?usp=sharing) +- [**Using Boundary-based Influence**](https://colab.research.google.com/drive/1785eHgT91FfqG1f25s7ovqd6JhP5uklh?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1785eHgT91FfqG1f25s7ovqd6JhP5uklh?usp=sharing) ## 🚀 Quick Start @@ -133,15 +135,17 @@ data_and_influence_dataset = influence_calculator.estimate_influence_values_grou All the influence calculation methods work on Tensorflow models trained for any sort of task and on any type of data. Visualization functionality is implemented for image datasets only (for the moment). -| **Influence Method** | Source | Tutorial | -|:--------------------------------------------------------| :---------------------------------------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------:| -| Influence Functions | [Paper](https://arxiv.org/abs/1703.04730) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1WlYcQNu5obhVjhonN2QYi8ybKyZJl4iY?usp=sharing) | -| RelatIF | [Paper](https://arxiv.org/pdf/2003.11630.pdf) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1WlYcQNu5obhVjhonN2QYi8ybKyZJl4iY?usp=sharing) | -| Influence Functions (first order, groups) | [Paper](https://arxiv.org/abs/1905.13289) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1WlYcQNu5obhVjhonN2QYi8ybKyZJl4iY?usp=sharing) | -| Influence Functions (second order, groups) | [Paper](https://arxiv.org/abs/1911.00418) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1qNvKiU3-aZWhRA0rxS6X3ebeNkoznJJe?usp=sharing) | -| Representer Point Selection (L2) | [Paper](https://arxiv.org/abs/1811.09720) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/17W5s30LbxABbDd8hbdwYE56abyWjSC4u?usp=sharing) | -| Representer Point Selection (Local Jacobian Expansion) | [Paper](https://proceedings.neurips.cc/paper/2021/file/c460dc0f18fc309ac07306a4a55d2fd6-Paper.pdf) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/14e7wwFRQJhY-huVYmJ7ri355kfLJgAPA?usp=sharing) | -| Trac-In | [Paper](https://arxiv.org/abs/2002.08484) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1E94cGF46SUQXcCTNwQ4VGSjXEKm7g21c?usp=sharing) | +| **Influence Method** | Source | Tutorial | +|:--------------------------------------------------------|:---------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------:| +| Influence Functions | [Paper](https://arxiv.org/abs/1703.04730) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1WlYcQNu5obhVjhonN2QYi8ybKyZJl4iY?usp=sharing) | +| RelatIF | [Paper](https://arxiv.org/pdf/2003.11630.pdf) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1WlYcQNu5obhVjhonN2QYi8ybKyZJl4iY?usp=sharing) | +| Influence Functions (first order, groups) | [Paper](https://arxiv.org/abs/1905.13289) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1WlYcQNu5obhVjhonN2QYi8ybKyZJl4iY?usp=sharing) | +| Influence Functions (second order, groups) | [Paper](https://arxiv.org/abs/1911.00418) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1qNvKiU3-aZWhRA0rxS6X3ebeNkoznJJe?usp=sharing) | +| Arnoldi iteration (Scaling Up Influence Functions) | [Paper](https://arxiv.org/abs/2112.03052) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1rQU33sbD0YW1cZMRlJmS15EW5O16yoDE?usp=sharing) | +| Representer Point Selection (L2) | [Paper](https://arxiv.org/abs/1811.09720) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/17W5s30LbxABbDd8hbdwYE56abyWjSC4u?usp=sharing) | +| Representer Point Selection (Local Jacobian Expansion) | [Paper](https://proceedings.neurips.cc/paper/2021/file/c460dc0f18fc309ac07306a4a55d2fd6-Paper.pdf) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/14e7wwFRQJhY-huVYmJ7ri355kfLJgAPA?usp=sharing) | +| Trac-In | [Paper](https://arxiv.org/abs/2002.08484) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1E94cGF46SUQXcCTNwQ4VGSjXEKm7g21c?usp=sharing) | +| Boundary-based influence | -- | - [**Using Boundary-based Influence**](https://colab.research.google.com/drive/1785eHgT91FfqG1f25s7ovqd6JhP5uklh?usp=sharing) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1785eHgT91FfqG1f25s7ovqd6JhP5uklh?usp=sharing) | ## 👀 See Also From c7179a7a156516418099b049b2b3556eb706ee61 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Wed, 29 Nov 2023 17:54:00 +0100 Subject: [PATCH 08/26] utils: add a function to split model into two --- deel/influenciae/utils/__init__.py | 3 ++- deel/influenciae/utils/tf_operations.py | 28 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/deel/influenciae/utils/__init__.py b/deel/influenciae/utils/__init__.py index 51f765b..f7a45a6 100644 --- a/deel/influenciae/utils/__init__.py +++ b/deel/influenciae/utils/__init__.py @@ -16,7 +16,8 @@ default_process_batch, dataset_to_tensor, array_to_dataset, - map_to_device + map_to_device, + split_model, ) from .sorted_dict import BatchSort, ORDER from .nearest_neighbors import BaseNearestNeighbors, LinearNearestNeighbors diff --git a/deel/influenciae/utils/tf_operations.py b/deel/influenciae/utils/tf_operations.py index ecd6048..0d5d896 100644 --- a/deel/influenciae/utils/tf_operations.py +++ b/deel/influenciae/utils/tf_operations.py @@ -8,6 +8,7 @@ import numpy as np import tensorflow as tf +from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2 from ..types import Union, Tuple, Optional, Callable @@ -57,6 +58,33 @@ def from_layer_name_to_layer_idx(model: tf.keras.Model, layer_name: str) -> int: raise ValueError(f'No such layer: {layer_name}. Existing layers are: ' f'{list(layer.name for layer in model.layers)}.') +def split_model(model: tf.keras.Model, target_layer: Union[str, int]) -> Tuple[tf.keras.Model, tf.keras.Model]: + """ + Splits a model into two sub-models, one containing the layers up to the target layer, and the other containing + the layers from the target layer onwards. + + Parameters + ---------- + model + Model to split + target_layer + Layer name or layer index + + Returns + ------- + model_1 + Model containing the layers up to the target layer + model_2 + Model containing the layers from the target layer onwards + """ + cloned_model = tf.keras.models.clone_model(model) + cloned_model.set_weights(model.get_weights()) + cut_layer = find_layer(cloned_model, target_layer) + model_1 = tf.keras.Model(inputs=cloned_model.inputs, outputs=cut_layer.input) + model_2 = tf.keras.Model(inputs=tf.keras.Input(tensor=cut_layer.input), outputs=cloned_model.outputs) + + return model_1, model_2 + def is_dataset_batched(dataset: tf.data.Dataset) -> Union[int, bool]: """ From 97c5c57e7146140e44415b1c1a047c213128a987 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Fri, 15 Dec 2023 15:34:27 +0100 Subject: [PATCH 09/26] utils: add a test for the model splitting function --- tests/utils/test_tf_operations.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/utils/test_tf_operations.py b/tests/utils/test_tf_operations.py index 9ac5db5..810cf98 100644 --- a/tests/utils/test_tf_operations.py +++ b/tests/utils/test_tf_operations.py @@ -6,7 +6,8 @@ import tensorflow as tf from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense -from deel.influenciae.utils import find_layer, is_dataset_batched, dataset_to_tensor, array_to_dataset +from deel.influenciae.utils import find_layer, is_dataset_batched, dataset_to_tensor, array_to_dataset, split_model +from ..utils_test import almost_equal def test_find_layer(): @@ -68,3 +69,21 @@ def test_array_to_dataset(): r_ds = array_to_dataset(r, batch_size=5, shuffle=True, buffer_size=25) r_sorted = tf.sort(tf.concat([b for b in r_ds], axis=0)) assert tf.reduce_all(r == r_sorted) + + +def test_split_model(): + """Ensure we can properly split a model into two sub-models""" + x = tf.random.normal((25, 32, 32, 3)) + model = tf.keras.models.Sequential([ + Conv2D(4, (1, 1), name="conv2d_1", input_shape=(32, 32, 3)), + Conv2D(4, (1, 1), name="conv2d_2"), + MaxPooling2D(), + Flatten(), + Dense(5) + ]) + model_a, model_b = split_model(model, 2) + model_a_test = tf.keras.models.Sequential(model.layers[:2]) + model_b_test = tf.keras.models.Sequential(model.layers[2:]) + assert almost_equal(model_a_test.predict(x), model_a.predict(x)) + assert almost_equal(model_b_test.predict(model_a_test.predict(x)), model_b.predict(model_a.predict(x))) + assert almost_equal(model(x), model_b(model_a(x))) From 21b74cf9ab954a6c4603fe05c84b18b621298f6e Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Thu, 4 Jan 2024 14:15:54 +0100 Subject: [PATCH 10/26] tests: add a util function for relative comparisons --- tests/utils_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/utils_test.py b/tests/utils_test.py index 67f3c2d..d840b2c 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -16,6 +16,11 @@ def almost_equal(arr1, arr2, epsilon=1e-6): return np.sum(np.abs(arr1 - arr2)) < epsilon +def relative_almost_equal(arr1, arr2, percent=0.01): + """Ensure two array are almost equal at a percent""" + return np.sum(np.abs(arr1 - arr2)) / np.sum(np.abs(arr1)) < percent + + def assert_tensor_equal(tensor1, tensor2): return tf.debugging.assert_equal(tensor1, tensor2) From ce5f8129bc0f9a34ed1b32e1526db9dac94738b7 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Tue, 30 Jan 2024 12:12:21 +0100 Subject: [PATCH 11/26] bench: add a use_bias parameter for the last layer of the model --- benchmark_runner.py | 3 +++ .../benchmark/cifar10_benchmark.py | 23 ++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/benchmark_runner.py b/benchmark_runner.py index 5ec31cc..08f8ef4 100644 --- a/benchmark_runner.py +++ b/benchmark_runner.py @@ -75,10 +75,13 @@ args = parser.parse_args() + use_bias = False if args.method_name == "rps_lje" or args.method_name == "rps_l2" else True + cifar10_evaluator = Cifar10MislabelingDetectorEvaluator(epochs=args.epochs, model_type=args.model_type, mislabeling_ratio=args.mislabeling_ratio, use_regu=args.use_regu, + use_bias=use_bias, force_overfit=args.force_overfit, train_batch_size=args.train_batch_size, test_batch_size=args.test_batch_size, diff --git a/deel/influenciae/benchmark/cifar10_benchmark.py b/deel/influenciae/benchmark/cifar10_benchmark.py index 589a06b..b155275 100644 --- a/deel/influenciae/benchmark/cifar10_benchmark.py +++ b/deel/influenciae/benchmark/cifar10_benchmark.py @@ -44,7 +44,7 @@ class ConvNetCIFAR(Sequential): use_regularization A boolean indicating whether to add regularization on the final model's last layer. """ - def __init__(self, model: Union[str, Model], use_regularization: bool = True, **kwargs): + def __init__(self, model: Union[str, Model], use_regularization: bool = True, use_bias: bool = True, **kwargs): super().__init__(**kwargs) if isinstance(model, Model): base_model = model @@ -73,9 +73,14 @@ def __init__(self, model: Union[str, Model], use_regularization: bool = True, ** self.add(tf.keras.layers.LeakyReLU()) if use_regularization: - dense_2 = Dense(10, kernel_regularizer=L1L2(l1=1e-4, l2=1e-4), kernel_initializer="he_normal") + dense_2 = Dense( + 10, + kernel_regularizer=L1L2(l1=1e-4, l2=1e-4), + kernel_initializer="he_normal", + use_bias=use_bias + ) else: - dense_2 = Dense(10) + dense_2 = Dense(10, use_bias=use_bias) self.add(dense_2) @@ -92,6 +97,8 @@ class Cifar10TrainingProcedure(BaseTrainingProcedure): A string with the type of model to use. Either 'resnet', 'efficient_net' or 'vgg19'. use_regu A boolean indicating whether L1L2 regularization should be used on the last layer. + use_bias + A boolean for adding a bias to the last layer. force_overfit A boolean for if the training schedule to be used should try to overfit the model or not. epochs_to_save @@ -107,6 +114,7 @@ def __init__( epochs: int = 60, model_type: str = 'resnet', use_regu: bool = True, + use_bias: bool = True, force_overfit: bool = False, epochs_to_save: Optional[List[int]] = None, verbose: bool = True, @@ -115,6 +123,7 @@ def __init__( self.epochs = epochs self.model_type = model_type self.use_regu = use_regu + self.use_bias = use_bias self.force_overfit = force_overfit self.epochs_to_save = epochs_to_save self.verbose = verbose @@ -171,7 +180,7 @@ def preprocess(x): test_dataset = test_dataset.batch(test_batch_size).prefetch(100) - model = ConvNetCIFAR(self.model_type, self.use_regu) + model = ConvNetCIFAR(self.model_type, self.use_regu, self.use_bias) loss = CategoricalCrossentropy(from_logits=True) @@ -266,6 +275,7 @@ def __init__( model_type: str = 'resnet', mislabeling_ratio: float = 0.0005, use_regu: bool = True, + use_bias: bool = True, force_overfit: bool = False, train_batch_size: int = 128, test_batch_size: int = 128, @@ -281,6 +291,7 @@ def __init__( "model_type": model_type, "mislabeling_ratio": mislabeling_ratio, "use_regularization": use_regu, + "use_bias": use_bias, "optimizer": 'sgd' if force_overfit else 'adam', "train_batch_size": train_batch_size, "test_batch_size": test_batch_size, @@ -301,8 +312,8 @@ def __init__( if take_batch is not None: training_dataset = training_dataset.take(take_batch) test_dataset = test_dataset.take(take_batch) - training_procedure = Cifar10TrainingProcedure(epochs, model_type, use_regu, force_overfit, epochs_to_save, - verbose_training, use_tensorboard) + training_procedure = Cifar10TrainingProcedure(epochs, model_type, use_regu, use_bias, force_overfit, + epochs_to_save, verbose_training, use_tensorboard) super().__init__(training_dataset, test_dataset, training_procedure, nb_classes=10, mislabeling_ratio=mislabeling_ratio, train_batch_size=train_batch_size, From 7947eec30703d0f85e8ded39912fc9bda074aec0 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Fri, 2 Feb 2024 16:25:35 +0100 Subject: [PATCH 12/26] inf_factory: modify the dataset operation on rps_lje to keep batch info for asserts --- deel/influenciae/benchmark/influence_factory.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/deel/influenciae/benchmark/influence_factory.py b/deel/influenciae/benchmark/influence_factory.py index 82de13e..ddb8e80 100644 --- a/deel/influenciae/benchmark/influence_factory.py +++ b/deel/influenciae/benchmark/influence_factory.py @@ -202,9 +202,7 @@ def build(self, training_dataset: tf.data.Dataset, model: tf.keras.Model, dataset_hessian = training_dataset else: batch_size = training_dataset._batch_size.numpy() # pylint: disable=W0212 - take_size = int( - np.ceil(float(self.dataset_hessian_size) / batch_size)) * batch_size - dataset_hessian = training_dataset.take(take_size) + dataset_hessian = training_dataset.unbatch().take(self.dataset_hessian_size).batch(batch_size) if self.ihvp_mode == 'exact': ihvp_calculator_factory = ExactIHVPFactory() From 55bffb191a10847943d43133c16bb93c1c778f99 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Fri, 2 Feb 2024 16:26:33 +0100 Subject: [PATCH 13/26] rps lje: fix the implementation to follow the original paper --- deel/influenciae/rps/rps_lje.py | 319 +++++++++++++++++++++++++------- 1 file changed, 253 insertions(+), 66 deletions(-) diff --git a/deel/influenciae/rps/rps_lje.py b/deel/influenciae/rps/rps_lje.py index 814084e..9844975 100644 --- a/deel/influenciae/rps/rps_lje.py +++ b/deel/influenciae/rps/rps_lje.py @@ -7,27 +7,23 @@ but using a local jacobian expansion, as per https://proceedings.neurips.cc/paper/2021/file/c460dc0f18fc309ac07306a4a55d2fd6-Paper.pdf """ -import itertools +from typing import Tuple import tensorflow as tf -from tensorflow.keras.models import Sequential # pylint: disable=E0611 +from tensorflow.keras.models import Sequential # pylint: disable=E0611 -from ..common import InfluenceModel -from ..common import InverseHessianVectorProductFactory - -from ..influence import FirstOrderInfluenceCalculator +from ..common import InfluenceModel, InverseHessianVectorProductFactory, BaseInfluenceCalculator +from ..utils import map_to_device, split_model, assert_batched_dataset from ..types import Union, Optional -class RepresenterPointLJE(FirstOrderInfluenceCalculator): +class RepresenterPointLJE(BaseInfluenceCalculator): """ Representer Point Selection via Local Jacobian Expansion for Post-hoc Classifier Explanation of Deep Neural Networks and Ensemble Models https://proceedings.neurips.cc/paper/2021/file/c460dc0f18fc309ac07306a4a55d2fd6-Paper.pdf - As this technique is quite similar to the implementation in - deel.influenciae.influence.first_order_influence_calculator from a functional point of view, we will re-use - it here. + Disclaimer: This technique requires the last layer of the model to be a Dense layer with no bias. Parameters ---------- @@ -44,6 +40,8 @@ class RepresenterPointLJE(FirstOrderInfluenceCalculator): Either a string or an integer identifying the layer on which to compute the influence-related quantities. shuffle_buffer_size An integer with the buffer size for the training set's shuffle operation. + epsilon + An epsilon value to prevent division by zero. """ def __init__( self, @@ -52,64 +50,57 @@ def __init__( ihvp_calculator_factory: InverseHessianVectorProductFactory, n_samples_for_hessian: Optional[int] = None, target_layer: Union[int, str] = -1, - shuffle_buffer_size: int = 10000 + shuffle_buffer_size: int = 10000, + epsilon: float = 1e-5 ): - # Use a FirstOrderInfluenceCalculator to compute the jacobian expanded weights for the model - ihvp_calculator = ihvp_calculator_factory.build(influence_model, dataset) - first_order_calculator = FirstOrderInfluenceCalculator(model=influence_model, - dataset=dataset, - ihvp_calculator=ihvp_calculator, - n_samples_for_hessian=n_samples_for_hessian, - shuffle_buffer_size=shuffle_buffer_size, - normalize=False) - influence_vector_dataset = first_order_calculator.compute_influence_vector(dataset) - - # Compute weight factor for the optimization step - size = tf.data.experimental.cardinality(dataset) - iter_dataset = iter(influence_vector_dataset) - weight_size = tf.reduce_sum( - tf.stack([tf.reduce_prod(tf.shape(w)) for w in influence_model.model.layers[target_layer].weights]) - ) + # Make sure that the model's last layer is a Dense layer with no bias + if not isinstance(influence_model.model.layers[-1], tf.keras.layers.Dense): + raise ValueError('The last layer of the model must be a Dense layer with no bias.') + if influence_model.model.layers[-1].use_bias: + raise ValueError('The last layer of the model must be a Dense layer with no bias.') + + # Make sure that the dataset is batched + assert_batched_dataset(dataset) + + self.target_layer = target_layer + self.epsilon = tf.constant(epsilon, dtype=tf.float32) + + # In the paper, the authors explain that in practice, they use a single step of SGD to compute the + # perturbed model's weights. We will do the same here. + optimizer = tf.keras.optimizers.SGD(learning_rate=1e-4) + feature_extractor, perturbed_head = split_model(influence_model.model, target_layer) + target_layer_shape = influence_model.model.layers[target_layer].input.type_spec.shape + perturbed_head.build(target_layer_shape) + perturbed_head.compile(optimizer=optimizer, loss=influence_model.loss_function) - def body(i, v, nb): - current_vector = next(iter_dataset)[1] - nb_next = nb + tf.cast(tf.shape(current_vector)[0], dtype=nb.dtype) - v_current = tf.reduce_sum(current_vector, axis=0) - v_next = (nb / nb_next) * v + v_current / nb_next - - return i + tf.constant(1, dtype=size.dtype), v_next, nb_next - - dtype_ = dataset.element_spec[0].dtype - _, influence_vector, __ = tf.while_loop(cond=lambda i, v, nb: i < size, - body=body, - loop_vars=[tf.constant(0, dtype=size.dtype), - tf.zeros((weight_size,), dtype=dtype_), - tf.constant(0.0, dtype=dtype_)]) - - # Extract the model's target weights and clone the model to update it - layers_end = influence_model.model.layers[target_layer:] - weights = [lay.weights for lay in layers_end] - weights = list(itertools.chain(*weights)) - model_end = tf.keras.models.clone_model(Sequential(layers_end)) - - # Update the new model - input_layer_shape = influence_model.model.layers[target_layer].input.type_spec.shape - model_end.build(input_layer_shape) - model_end.set_weights(weights) - self._reshape_assign(model_end.layers[0].weights, influence_vector) - - # Instantiate the elements for calculating the influence through the FirstOrderInfluenceCalculator's - features_extractor = Sequential(influence_model.model.layers[:target_layer]) - model = InfluenceModel(Sequential([features_extractor, model_end]), 1, - loss_function=influence_model.loss_function) - ihvp_calculator = ihvp_calculator_factory.build(model, dataset) - - super().__init__(model=model, - dataset=dataset, - ihvp_calculator=ihvp_calculator, - n_samples_for_hessian=n_samples_for_hessian, - shuffle_buffer_size=shuffle_buffer_size, - normalize=False) + # Get a dataset to compute the SGD step + if n_samples_for_hessian is None: + dataset_to_estimate_hessian = map_to_device(dataset, lambda x, y: (feature_extractor(x), y)) + else: + dataset_to_estimate_hessian = map_to_device( + dataset.shuffle(shuffle_buffer_size).take(n_samples_for_hessian), + lambda x, y: (feature_extractor(x), y) + ) + + # Accumulate the gradients for the whole dataset and then update + trainable_vars = perturbed_head.trainable_variables + accum_vars = [tf.Variable(tf.zeros_like(t_var.read_value()), trainable=False) + for t_var in trainable_vars] + for x, y in dataset_to_estimate_hessian: + with tf.GradientTape() as tape: + y_pred = perturbed_head(x) + loss = -perturbed_head.loss(y, y_pred) + gradients = tape.gradient(loss, trainable_vars) + _ = [accum_vars[i].assign_add(grad) for i, grad in enumerate(gradients)] + optimizer.apply_gradients(zip(accum_vars, trainable_vars)) + + # Keep the feature extractor and the perturbed head + self.feature_extractor = feature_extractor + self.perturbed_head = perturbed_head + + # Create the new model with the perturbed weights to compute the hessian matrix + model = InfluenceModel(self.perturbed_head, 1, loss_function=influence_model.loss_function) # layer 0 is InputLayer + self.ihvp_calculator = ihvp_calculator_factory.build(model, dataset_to_estimate_hessian) def _reshape_assign(self, weights, influence_vector: tf.Tensor) -> None: """ @@ -130,3 +121,199 @@ def _reshape_assign(self, weights, influence_vector: tf.Tensor) -> None: index += size v = tf.reshape(v, shape) w.assign(w - v) + + def _preprocess_samples(self, samples: Tuple[tf.Tensor, ...]) -> tf.Tensor: + """ + Preprocess a single batch of samples. + + Parameters + ---------- + samples + A single batch of tensors containing the samples. + + Returns + ------- + evaluate_vect + The preprocessed sample + """ + z_batch = self.feature_extractor(samples[:-1]) + y_batch = samples[-1] + + return z_batch, y_batch + + def _compute_alpha(self, z_batch: tf.Tensor, y_batch: tf.Tensor) -> tf.Tensor: + """ + Computes the alpha vector for the Local Jacobian Expansion approximation. + + Parameters + ---------- + z_batch + A tensor with the perturbed model's predictions. + y_batch + A tensor with the ground truth labels. + + Returns + ------- + A tensor with the alpha vector for the Local Jacobian Expansion approximation. + """ + # First, we compute the second term, which contains the Hessian vector product + weights = self.perturbed_head.trainable_weights + with tf.GradientTape(persistent=False, watch_accessed_variables=False) as tape: + tape.watch(weights) + logits = self.perturbed_head(z_batch) + loss = self.perturbed_head.compiled_loss(y_batch, logits) + grads = tape.jacobian(loss, weights)[0] + grads = tf.multiply( + grads, + tf.repeat( + tf.expand_dims( + tf.divide(tf.ones_like(z_batch), + tf.cast(tf.shape(z_batch)[0], z_batch.dtype) * z_batch + + tf.cast(self.epsilon, z_batch.dtype)), + axis=-1), + grads.shape[-1], axis=-1 + ) + ) + second_term = tf.map_fn( + lambda v: self.ihvp_calculator._compute_ihvp_single_batch(tf.expand_dims(v, axis=0), use_gradient=False), + grads + ) # pylint: disable=protected-access + second_term = tf.reduce_sum(tf.reshape(second_term, tf.shape(grads)), axis=1) + + # Second, we compute the first term, which contains the weights + first_term = tf.concat([w for w in weights], axis=0) + first_term = tf.multiply( + first_term, + tf.repeat( + tf.expand_dims( + tf.divide(tf.ones_like(z_batch), + tf.cast(tf.shape(z_batch)[0], z_batch.dtype) * z_batch + + tf.cast(self.epsilon, z_batch.dtype)), + axis=-1), + first_term.shape[-1], axis=-1 + ) + ) + first_term = tf.reduce_sum(first_term, axis=1) + + return first_term - second_term # alpha is first term minus second term + + def _compute_influence_value_from_batch(self, train_samples: Tuple[tf.Tensor, ...]) -> tf.Tensor: + """ + Compute the influence score for a batch of training samples (i.e. self-influence). + + Parameters + ---------- + train_samples + A tensor containing a batch of training samples. + + Returns + ------- + influence_values + A tensor with the self-influence of the training samples. + """ + z_batch, y_batch = self._preprocess_samples(train_samples) + alpha = self._compute_alpha(z_batch, y_batch) + + # If the problem is binary classification, take all the alpha values + # If multiclass, take only those that correspond to the prediction + out_shape = self.perturbed_head.output_shape + if len(out_shape) == 1: + influence_values = alpha + elif len(out_shape) == 2 and out_shape[1] == 1: + influence_values = alpha + else: + if len(out_shape) > 2: + indices = tf.argmax(tf.squeeze(self.perturbed_head(z_batch), axis=-1), axis=1) + else: + indices = tf.argmax(self.perturbed_head(z_batch), axis=1) + influence_values = tf.gather(alpha, indices, axis=1, batch_dims=1) + + return influence_values + + def _compute_influence_vector(self, train_samples: Tuple[tf.Tensor, ...]) -> tf.Tensor: + """ + Compute an equivalent of the influence vector for a sample of training points. + + Disclaimer: this vector is not an estimation of the difference between the actual + model and the perturbed model without the samples (like it is the case with what is + calculated using deel.influenciae.influence). + + Parameters + ---------- + train_samples + A tensor with a group of training samples of which we wish to compute the influence. + + Returns + ------- + influence_vectors + A tensor with a concatenation of the alpha weights and the feature maps for each sample. + This allows for optimizations to be put in place but is not really an influence vector + of any kind. + """ + z_batch = self.feature_extractor(train_samples[:-1]) + alpha = self._compute_alpha(z_batch, train_samples[-1]) + + return alpha, z_batch + + def _estimate_individual_influence_values_from_batch( + self, + train_samples: Tuple[tf.Tensor, ...], + samples_to_evaluate: Tuple[tf.Tensor, ...] + ) -> tf.Tensor: + """ + Estimate the (individual) influence scores of a single batch of samples with respect to + a batch of samples belonging to the model's training dataset. + + Parameters + ---------- + train_samples + A single batch of training samples (and their target values). + samples_to_evaluate + A single batch of samples of which we wish to compute the influence of removing the training + samples. + + Returns + ------- + A tensor containing the individual influence scores. + """ + return self._estimate_influence_value_from_influence_vector( + self._preprocess_samples(samples_to_evaluate), + self._compute_influence_vector(train_samples) + ) + + def _estimate_influence_value_from_influence_vector( + self, + preproc_test_sample: tf.Tensor, + influence_vector: tf.Tensor + ) -> tf.Tensor: + """ + Compute the influence score for a (batch of) preprocessed test sample(s) and a training "influence vector". + + Parameters + ---------- + preproc_test_sample + A tensor with a pre-processed sample to evaluate. + influence_vector + A tensor with the training influence vector. + + Returns + ------- + influence_values + A tensor with influence values for the (batch of) test samples. + """ + # Extract the different information inside the tuples + feature_maps_test, labels_test = preproc_test_sample + alpha, feature_maps_train = influence_vector + + if len(alpha.shape) == 1 or (len(alpha.shape) == 2 and alpha.shape[1] == 1): + influence_values = alpha * tf.matmul(feature_maps_train, feature_maps_test, transpose_b=True) + else: + if len(self.perturbed_head.output_shape) > 2: + indices = tf.argmax(tf.squeeze(self.perturbed_head(feature_maps_test), axis=-1), axis=1) + else: + indices = tf.argmax(self.perturbed_head(feature_maps_test), axis=1) + influence_values = tf.gather(alpha, indices, axis=1, batch_dims=1) * \ + tf.matmul(feature_maps_train, feature_maps_test, transpose_b=True) + influence_values = tf.transpose(influence_values) + + return influence_values From 6289e71c978effaeb4902e8d185a144dc1c251d6 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Fri, 2 Feb 2024 16:26:57 +0100 Subject: [PATCH 14/26] tests: update rps_lje tests to follow implementation change --- tests/rps/test_rps_lje.py | 372 +++++++++++++++++++++++--------------- 1 file changed, 228 insertions(+), 144 deletions(-) diff --git a/tests/rps/test_rps_lje.py b/tests/rps/test_rps_lje.py index f0da0e2..443b1d3 100644 --- a/tests/rps/test_rps_lje.py +++ b/tests/rps/test_rps_lje.py @@ -5,86 +5,154 @@ import tensorflow as tf from tensorflow.keras.layers import Input, Conv2D, Dense, Flatten from tensorflow.keras.models import Sequential -from tensorflow.keras.losses import Reduction, MeanSquaredError, BinaryCrossentropy +from tensorflow.keras.losses import Reduction, CategoricalCrossentropy, BinaryCrossentropy from deel.influenciae.common import InfluenceModel from deel.influenciae.common import ExactIHVP, ExactIHVPFactory from deel.influenciae.rps import RepresenterPointLJE -from deel.influenciae.influence import FirstOrderInfluenceCalculator -from ..utils_test import assert_inheritance +from ..utils_test import assert_inheritance, almost_equal, relative_almost_equal -def test_compute_influence_vector(): + +def test_alpha(): tf.random.set_seed(0) - model_feature = Sequential() - model_feature.add(Input(shape=(5, 5, 3), dtype=tf.float64)) - model_feature.add(Conv2D(4, kernel_size=(2, 2), - activation='relu', dtype=tf.float64)) - model_feature.add(Flatten(dtype=tf.float64)) - - binary = True - if binary: - model = Sequential( - [model_feature, Dense(1, use_bias=False, dtype=tf.float64, activation='sigmoid')]) - loss_function = BinaryCrossentropy(reduction=Reduction.NONE) - else: - model = Sequential( - [model_feature, Dense(1, use_bias=False, dtype=tf.float64)]) - loss_function = MeanSquaredError(reduction=Reduction.NONE) + model = Sequential() + model.add(Input(shape=(5, 5, 3), dtype=tf.float64)) + model.add(Conv2D(4, kernel_size=(2, 2), + activation='relu', dtype=tf.float64)) + model.add(Flatten(dtype=tf.float64)) + model.add(Dense(4, use_bias=False, dtype=tf.float64)) + loss_function = CategoricalCrossentropy(from_logits=True, reduction=Reduction.NONE) model(tf.random.normal((50, 5, 5, 3), dtype=tf.float64)) inputs_train = tf.random.normal((50, 5, 5, 3), dtype=tf.float64) - targets_train = tf.random.normal((50, 1), dtype=tf.float64) + targets_train = tf.random.normal((50, 4), dtype=tf.float64) train_dataset = tf.data.Dataset.from_tensor_slices((inputs_train, targets_train)).batch(5) - influence_model = InfluenceModel(model, start_layer=-1, loss_function=loss_function) + target_layer = -1 + influence_model = InfluenceModel(model, start_layer=target_layer, loss_function=loss_function) rps_lje = RepresenterPointLJE(influence_model, train_dataset, ExactIHVPFactory(), target_layer=-1) - ihvp_computed = rps_lje._compute_influence_vector((inputs_train, targets_train)) - influence_model = InfluenceModel(model, start_layer=-1, loss_function=loss_function) - ihvp_calculator = ExactIHVP(influence_model, train_dataset) - first_order = FirstOrderInfluenceCalculator(influence_model, train_dataset, ihvp_calculator) + # Compute alpha using rps_lje + feature_extractor = Sequential(model.layers[:target_layer]) + feature_maps = feature_extractor(inputs_train) + alpha = rps_lje._compute_alpha(feature_maps, targets_train) + + # Compute alpha manually + # First, create the perturbed model + optimizer = tf.keras.optimizers.SGD(learning_rate=1e-4) + perturbed_model = Sequential(model.layers[target_layer:]) + perturbed_model.build(input_shape=feature_extractor.output_shape) + with tf.GradientTape() as tape: + tape.watch(perturbed_model.weights) + logits = perturbed_model(feature_maps) + loss = tf.reduce_mean(-loss_function(targets_train, logits)) + grads = tape.gradient(loss, perturbed_model.weights) + optimizer.apply_gradients(zip(grads, perturbed_model.weights)) + + # Now, we can compute alpha + # Start with the second term + dataset_for_hessian = tf.data.Dataset.from_tensor_slices((feature_maps, targets_train)).batch(5) + ihvp = ExactIHVP(InfluenceModel(perturbed_model, start_layer=0, loss_function=loss_function), dataset_for_hessian) + with tf.GradientTape() as tape: + tape.watch(perturbed_model.weights) + logits = perturbed_model(feature_maps) + loss = loss_function(targets_train, logits) + grads = tape.jacobian(loss, perturbed_model.weights)[0] + + # Divide grads by feature maps + grads_div_feature_maps = [] + for i in range(inputs_train.shape[0]): + feature_map = tf.reshape(feature_maps[i], (-1, 1)) if len(feature_maps[i].shape) == 1 else feature_maps[i] + divisor = tf.tile( + tf.cast(tf.shape(feature_map)[0], feature_map.dtype) * feature_map + + tf.constant(1e-5, dtype=feature_map.dtype), + (1, grads.shape[-1]) + ) + grads_div_feature_maps.append(tf.divide(grads[i], divisor)) + grads_div_feature_maps = tf.convert_to_tensor(grads_div_feature_maps) + second_term = [] + for i in range(inputs_train.shape[0]): + second_term.append(ihvp._compute_ihvp_single_batch( + tf.expand_dims(grads_div_feature_maps[i], axis=0), use_gradient=False + )) + second_term = tf.convert_to_tensor(second_term) + second_term = tf.reshape(second_term, grads.shape) + second_term = tf.reduce_sum(second_term, axis=1) + + # Now, compute the first term + # first term is weights divided by feature maps + weights = [w for w in perturbed_model.weights] + first_term = [] + for i in range(inputs_train.shape[0]): + feature_map = tf.reshape(feature_maps[i], (-1, 1)) if len(feature_maps[i].shape) == 1 else feature_maps[i] + divisor = tf.tile( + tf.cast(tf.shape(feature_map)[0], feature_map.dtype) * feature_map + + tf.constant(1e-5, dtype=feature_map.dtype), + (1, grads.shape[-1]) + ) + first_term.append(tf.divide(weights, divisor)) + first_term = tf.convert_to_tensor(first_term) + first_term = tf.reshape(first_term, grads.shape) + first_term = tf.reduce_sum(first_term, axis=1) + + # Combine to get alpha_test + alpha_test = first_term - second_term + + assert alpha.shape == alpha_test.shape + assert relative_almost_equal(alpha, alpha_test, percent=0.1) # results tend to contain large numbers, relative makes more sense + - vect = first_order._compute_influence_vector((inputs_train, targets_train)) - weight = model.layers[-1].weights[0] - vect = tf.reshape(tf.reduce_mean(vect, axis=0), tf.shape(weight)) - weight.assign(weight - vect) +def test_compute_influence_vector(): + tf.random.set_seed(0) + + model = Sequential() + model.add(Input(shape=(5, 5, 3), dtype=tf.float64)) + model.add(Conv2D(4, kernel_size=(2, 2), + activation='relu', dtype=tf.float64)) + model.add(Flatten(dtype=tf.float64)) + model.add(Dense(4, use_bias=False, dtype=tf.float64)) + loss_function = CategoricalCrossentropy(from_logits=True, reduction=Reduction.NONE) + + model(tf.random.normal((50, 5, 5, 3), dtype=tf.float64)) + + inputs_train = tf.random.normal((50, 5, 5, 3), dtype=tf.float64) + targets_train = tf.random.normal((50, 4), dtype=tf.float64) + + train_dataset = tf.data.Dataset.from_tensor_slices((inputs_train, targets_train)).batch(5) influence_model = InfluenceModel(model, start_layer=-1, loss_function=loss_function) - ihvp_calculator = ExactIHVP(influence_model, train_dataset) - first_order = FirstOrderInfluenceCalculator(influence_model, train_dataset, ihvp_calculator) - ihvp_expected = first_order._compute_influence_vector((inputs_train, targets_train)) + rps_lje = RepresenterPointLJE(influence_model, train_dataset, ExactIHVPFactory(), target_layer=-1) + alpha, z_batch = rps_lje._compute_influence_vector((inputs_train, targets_train)) - assert tf.reduce_max(tf.abs((ihvp_computed - ihvp_expected) / ihvp_expected)) < 1E-2 + # Now, compute them manually to check that it is correct + feature_extractor = Sequential(model.layers[:-1]) + z_batch_test = feature_extractor(inputs_train) + alpha_test = rps_lje._compute_alpha(z_batch_test, targets_train) + + assert almost_equal(z_batch, z_batch_test) + assert almost_equal(alpha, alpha_test) # alpha is already tested somewhere else def test_preprocess_sample_to_evaluate(): tf.random.set_seed(0) - model_feature = Sequential() - model_feature.add(Input(shape=(5, 5, 3), dtype=tf.float64)) - model_feature.add(Conv2D(4, kernel_size=(2, 2), - activation='relu', dtype=tf.float64)) - model_feature.add(Flatten(dtype=tf.float64)) - - binary = True - if binary: - model = Sequential( - [model_feature, Dense(1, use_bias=False, dtype=tf.float64, activation='sigmoid')]) - loss_function = BinaryCrossentropy(reduction=Reduction.NONE) - else: - model = Sequential( - [model_feature, Dense(1, use_bias=False, dtype=tf.float64)]) - loss_function = MeanSquaredError(reduction=Reduction.NONE) + model = Sequential() + model.add(Input(shape=(5, 5, 3), dtype=tf.float64)) + model.add(Conv2D(4, kernel_size=(2, 2), + activation='relu', dtype=tf.float64)) + model.add(Flatten(dtype=tf.float64)) + model.add(Dense(4, use_bias=False, dtype=tf.float64)) + loss_function = CategoricalCrossentropy(from_logits=True, reduction=Reduction.NONE) model(tf.random.normal((50, 5, 5, 3), dtype=tf.float64)) inputs_train = tf.random.normal((50, 5, 5, 3), dtype=tf.float64) - targets_train = tf.random.normal((50, 1), dtype=tf.float64) + targets_train = tf.random.normal((50, 4), dtype=tf.float64) inputs_test = tf.random.normal((60, 5, 5, 3), dtype=tf.float64) targets_test = tf.random.normal((60, 1), dtype=tf.float64) @@ -95,147 +163,163 @@ def test_preprocess_sample_to_evaluate(): rps_lje = RepresenterPointLJE(influence_model, train_dataset, ExactIHVPFactory(), target_layer=-1) pre_evaluate_computed = rps_lje._preprocess_samples((inputs_test, targets_test)) - influence_model = InfluenceModel(model, start_layer=-1, loss_function=loss_function) - ihvp_calculator = ExactIHVP(influence_model, train_dataset) - first_order = FirstOrderInfluenceCalculator(influence_model, train_dataset, ihvp_calculator) + # Compute the feature maps + feature_extractor = Sequential(model.layers[:-1]) + feature_maps = feature_extractor(inputs_test) - vect = first_order._compute_influence_vector((inputs_train, targets_train)) - weight = model.layers[-1].weights[0] - vect = tf.reshape(tf.reduce_mean(vect, axis=0), tf.shape(weight)) - weight.assign(weight - vect) + # Check that we get the feature maps and the targets + assert almost_equal(pre_evaluate_computed[0], feature_maps) + assert almost_equal(pre_evaluate_computed[1], targets_test) - influence_model = InfluenceModel(model, start_layer=-1, loss_function=loss_function) - ihvp_calculator = ExactIHVP(influence_model, train_dataset) - first_order = FirstOrderInfluenceCalculator(influence_model, train_dataset, ihvp_calculator) - pre_evaluate_expected = first_order._preprocess_samples((inputs_test, targets_test)) - assert tf.reduce_max(tf.abs(pre_evaluate_computed - pre_evaluate_expected)) < 1E-3 - - -def test_compute_influence_value_from_influence_vector(): +def test_compute_influence_value_from_influence_vector_binary(): tf.random.set_seed(0) - model_feature = Sequential() - model_feature.add(Input(shape=(5, 5, 3), dtype=tf.float64)) - model_feature.add(Conv2D(4, kernel_size=(2, 2), - activation='relu', dtype=tf.float64)) - model_feature.add(Flatten(dtype=tf.float64)) - - binary = True - if binary: - model = Sequential( - [model_feature, Dense(1, use_bias=False, dtype=tf.float64, activation='sigmoid')]) - loss_function = BinaryCrossentropy(reduction=Reduction.NONE) - else: - model = Sequential( - [model_feature, Dense(1, use_bias=False, dtype=tf.float64)]) - loss_function = MeanSquaredError(reduction=Reduction.NONE) + model = Sequential() + model.add(Input(shape=(5, 5, 3), dtype=tf.float64)) + model.add(Conv2D(4, kernel_size=(2, 2), + activation='relu', dtype=tf.float64)) + model.add(Flatten(dtype=tf.float64)) + model.add(Dense(1, use_bias=False, dtype=tf.float64)) + loss_function = BinaryCrossentropy(from_logits=True, reduction=Reduction.NONE) model(tf.random.normal((50, 5, 5, 3), dtype=tf.float64)) inputs_train = tf.random.normal((50, 5, 5, 3), dtype=tf.float64) targets_train = tf.random.normal((50, 1), dtype=tf.float64) - inputs_test = tf.random.normal((60, 5, 5, 3), dtype=tf.float64) - targets_test = tf.random.normal((60, 1), dtype=tf.float64) - train_dataset = tf.data.Dataset.from_tensor_slices((inputs_train, targets_train)).batch(5) + # Compute the influence values using RPS-LJE influence_model = InfluenceModel(model, start_layer=-1, loss_function=loss_function) rps_lje = RepresenterPointLJE(influence_model, train_dataset, ExactIHVPFactory(), target_layer=-1) - v_test = rps_lje._preprocess_samples((inputs_test, targets_test)) - influence_vector = rps_lje._compute_influence_vector((inputs_test, targets_test)) - influence_values_computed = rps_lje._estimate_influence_value_from_influence_vector(v_test, influence_vector) + influence_values_computed = rps_lje._compute_influence_value_from_batch((inputs_train, targets_train)) - influence_model = InfluenceModel(model, start_layer=-1, loss_function=loss_function) - ihvp_calculator = ExactIHVP(influence_model, train_dataset) - first_order = FirstOrderInfluenceCalculator(influence_model, train_dataset, ihvp_calculator) + # Compute the influence values manually + alpha, z_batch = rps_lje._compute_influence_vector((inputs_train, targets_train)) # already checked in another test + influence_values = alpha + + assert almost_equal(influence_values_computed, influence_values, epsilon=1e-3) - vect = first_order._compute_influence_vector((inputs_train, targets_train)) - weight = model.layers[-1].weights[0] - vect = tf.reshape(tf.reduce_mean(vect, axis=0), tf.shape(weight)) - weight.assign(weight - vect) +def test_compute_influence_value_from_influence_vector_multiclass(): + tf.random.set_seed(0) + + model = Sequential() + model.add(Input(shape=(5, 5, 3), dtype=tf.float64)) + model.add(Conv2D(4, kernel_size=(2, 2), + activation='relu', dtype=tf.float64)) + model.add(Flatten(dtype=tf.float64)) + model.add(Dense(4, use_bias=False, dtype=tf.float64)) + loss_function = CategoricalCrossentropy(from_logits=True, reduction=Reduction.NONE) + + model(tf.random.normal((50, 5, 5, 3), dtype=tf.float64)) + + inputs_train = tf.random.normal((50, 5, 5, 3), dtype=tf.float64) + targets_train = tf.random.normal((50, 4), dtype=tf.float64) + + train_dataset = tf.data.Dataset.from_tensor_slices((inputs_train, targets_train)).batch(5) + + # Compute the influence values using RPS-LJE influence_model = InfluenceModel(model, start_layer=-1, loss_function=loss_function) - ihvp_calculator = ExactIHVP(influence_model, train_dataset) - first_order = FirstOrderInfluenceCalculator(influence_model, train_dataset, ihvp_calculator) - v_test = first_order._preprocess_samples((inputs_test, targets_test)) - influence_vector = first_order._compute_influence_vector((inputs_test, targets_test)) - influence_values_expected = first_order._estimate_influence_value_from_influence_vector(v_test, influence_vector) + rps_lje = RepresenterPointLJE(influence_model, train_dataset, ExactIHVPFactory(), target_layer=-1) + influence_values_computed = rps_lje._compute_influence_value_from_batch((inputs_train, targets_train)) + + # Compute the influence values manually + alpha, z_batch = rps_lje._compute_influence_vector((inputs_train, targets_train)) # already checked in another test + alpha_i = tf.gather(alpha, tf.argmax(rps_lje.perturbed_head(z_batch), axis=1), axis=1, batch_dims=1) + influence_values = alpha_i - assert tf.reduce_max( - tf.abs((influence_values_computed - influence_values_expected) / influence_values_expected)) < 1E-2 + assert almost_equal(influence_values_computed, influence_values, epsilon=1e-3) -def test_compute_pairwise_influence_value(): +def test_compute_pairwise_influence_value_binary(): tf.random.set_seed(0) - model_feature = Sequential() - model_feature.add(Input(shape=(5, 5, 3), dtype=tf.float64)) - model_feature.add(Conv2D(4, kernel_size=(2, 2), - activation='relu', dtype=tf.float64)) - model_feature.add(Flatten(dtype=tf.float64)) - - binary = True - if binary: - model = Sequential( - [model_feature, Dense(1, use_bias=False, dtype=tf.float64, activation='sigmoid')]) - loss_function = BinaryCrossentropy(reduction=Reduction.NONE) - else: - model = Sequential( - [model_feature, Dense(1, use_bias=False, dtype=tf.float64)]) - loss_function = MeanSquaredError(reduction=Reduction.NONE) + model = Sequential() + model.add(Input(shape=(5, 5, 3), dtype=tf.float64)) + model.add(Conv2D(4, kernel_size=(2, 2), + activation='relu', dtype=tf.float64)) + model.add(Flatten(dtype=tf.float64)) + model.add(Dense(1, use_bias=False, dtype=tf.float64)) + loss_function = BinaryCrossentropy(from_logits=True, reduction=Reduction.NONE) model(tf.random.normal((50, 5, 5, 3), dtype=tf.float64)) inputs_train = tf.random.normal((50, 5, 5, 3), dtype=tf.float64) targets_train = tf.random.normal((50, 1), dtype=tf.float64) - inputs_test = tf.random.normal((60, 5, 5, 3), dtype=tf.float64) - targets_test = tf.random.normal((60, 1), dtype=tf.float64) + inputs_test = tf.random.normal((50, 5, 5, 3), dtype=tf.float64) + targets_test = tf.random.normal((50, 1), dtype=tf.float64) train_dataset = tf.data.Dataset.from_tensor_slices((inputs_train, targets_train)).batch(5) influence_model = InfluenceModel(model, start_layer=-1, loss_function=loss_function) rps_lje = RepresenterPointLJE(influence_model, train_dataset, ExactIHVPFactory(), target_layer=-1) - influence_values_computed = rps_lje._compute_influence_value_from_batch((inputs_test, targets_test)) + v_test = rps_lje._preprocess_samples((inputs_test, targets_test)) + influence_vector = rps_lje._compute_influence_vector((inputs_train, targets_train)) + influence_values_computed = rps_lje._estimate_influence_value_from_influence_vector(v_test, influence_vector) - influence_model = InfluenceModel(model, start_layer=-1, loss_function=loss_function) - ihvp_calculator = ExactIHVP(influence_model, train_dataset) - first_order = FirstOrderInfluenceCalculator(influence_model, train_dataset, ihvp_calculator) + # Compute the values manually + feature_extractor = Sequential(model.layers[:-1]) + alpha_test = influence_vector[0] # alpha and influence vector are already tested somewhere else + feature_maps_train = feature_extractor(inputs_train) + feature_maps_test = feature_extractor(inputs_test) + influence_values_test = alpha_test * tf.matmul(feature_maps_train, feature_maps_test, transpose_b=True) + influence_values_test = tf.transpose(influence_values_test) - vect = first_order._compute_influence_vector((inputs_train, targets_train)) - weight = model.layers[-1].weights[0] - vect = tf.reshape(tf.reduce_mean(vect, axis=0), tf.shape(weight)) - weight.assign(weight - vect) + assert relative_almost_equal(influence_values_computed, influence_values_test, percent=0.1) + + +def test_compute_pairwise_influence_value_multiclass(): + tf.random.set_seed(0) + + model = Sequential() + model.add(Input(shape=(5, 5, 3), dtype=tf.float64)) + model.add(Conv2D(4, kernel_size=(2, 2), + activation='relu', dtype=tf.float64)) + model.add(Flatten(dtype=tf.float64)) + model.add(Dense(4, use_bias=False, dtype=tf.float64)) + loss_function = CategoricalCrossentropy(from_logits=True, reduction=Reduction.NONE) + + model(tf.random.normal((50, 5, 5, 3), dtype=tf.float64)) + + inputs_train = tf.random.normal((50, 5, 5, 3), dtype=tf.float64) + targets_train = tf.random.normal((50, 4), dtype=tf.float64) + + inputs_test = tf.random.normal((50, 5, 5, 3), dtype=tf.float64) + targets_test = tf.random.normal((50, 4), dtype=tf.float64) + + train_dataset = tf.data.Dataset.from_tensor_slices((inputs_train, targets_train)).batch(5) influence_model = InfluenceModel(model, start_layer=-1, loss_function=loss_function) - ihvp_calculator = ExactIHVP(influence_model, train_dataset) - first_order = FirstOrderInfluenceCalculator(influence_model, train_dataset, ihvp_calculator) - influence_values_expected = first_order._compute_influence_value_from_batch((inputs_test, targets_test)) + rps_lje = RepresenterPointLJE(influence_model, train_dataset, ExactIHVPFactory(), target_layer=-1) + v_test = rps_lje._preprocess_samples((inputs_test, targets_test)) + influence_vector = rps_lje._compute_influence_vector((inputs_train, targets_train)) + influence_values_computed = rps_lje._estimate_influence_value_from_influence_vector(v_test, influence_vector) + + # Compute the values manually + feature_extractor = Sequential(model.layers[:-1]) + feature_maps_train = feature_extractor(inputs_train) + feature_maps_test = feature_extractor(inputs_test) + indices = tf.argmax(rps_lje.perturbed_head(feature_maps_test), axis=1) + alpha_test = tf.gather(influence_vector[0], indices, axis=1, batch_dims=1) + influence_values_test = alpha_test * tf.matmul(feature_maps_train, feature_maps_test, transpose_b=True) + influence_values_test = tf.transpose(influence_values_test) - assert tf.reduce_max( - tf.abs((influence_values_computed - influence_values_expected) / influence_values_expected)) < 1E-2 + assert relative_almost_equal(influence_values_computed, influence_values_test, percent=0.1) def test_inheritance(): tf.random.set_seed(0) - model_feature = Sequential() - model_feature.add(Input(shape=(5, 5, 3), dtype=tf.float64)) - model_feature.add(Conv2D(4, kernel_size=(2, 2), + model = Sequential() + model.add(Input(shape=(5, 5, 3), dtype=tf.float64)) + model.add(Conv2D(4, kernel_size=(2, 2), activation='relu', dtype=tf.float64)) - model_feature.add(Flatten(dtype=tf.float64)) - - binary = True - if binary: - model = Sequential( - [model_feature, Dense(1, use_bias=False, dtype=tf.float64, activation='sigmoid')]) - loss_function = BinaryCrossentropy(reduction=Reduction.NONE) - else: - model = Sequential( - [model_feature, Dense(1, use_bias=False, dtype=tf.float64)]) - loss_function = MeanSquaredError(reduction=Reduction.NONE) + model.add(Flatten(dtype=tf.float64)) + model.add(Dense(1, use_bias=False, dtype=tf.float64)) + loss_function = BinaryCrossentropy(reduction=Reduction.NONE) model(tf.random.normal((10, 5, 5, 3), dtype=tf.float64)) From 47419a02851877cefa7e713616886a20df7b10d1 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Fri, 2 Feb 2024 16:50:01 +0100 Subject: [PATCH 15/26] lint: clean-up --- deel/influenciae/rps/rps_lje.py | 18 ++++++++++++------ deel/influenciae/utils/tf_operations.py | 1 - 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/deel/influenciae/rps/rps_lje.py b/deel/influenciae/rps/rps_lje.py index 9844975..0db673b 100644 --- a/deel/influenciae/rps/rps_lje.py +++ b/deel/influenciae/rps/rps_lje.py @@ -10,7 +10,6 @@ from typing import Tuple import tensorflow as tf -from tensorflow.keras.models import Sequential # pylint: disable=E0611 from ..common import InfluenceModel, InverseHessianVectorProductFactory, BaseInfluenceCalculator from ..utils import map_to_device, split_model, assert_batched_dataset @@ -99,7 +98,11 @@ def __init__( self.perturbed_head = perturbed_head # Create the new model with the perturbed weights to compute the hessian matrix - model = InfluenceModel(self.perturbed_head, 1, loss_function=influence_model.loss_function) # layer 0 is InputLayer + model = InfluenceModel( + self.perturbed_head, + 1, # layer 0 is InputLayer + loss_function=influence_model.loss_function + ) self.ihvp_calculator = ihvp_calculator_factory.build(model, dataset_to_estimate_hessian) def _reshape_assign(self, weights, influence_vector: tf.Tensor) -> None: @@ -175,13 +178,16 @@ def _compute_alpha(self, z_batch: tf.Tensor, y_batch: tf.Tensor) -> tf.Tensor: ) ) second_term = tf.map_fn( - lambda v: self.ihvp_calculator._compute_ihvp_single_batch(tf.expand_dims(v, axis=0), use_gradient=False), + lambda v: self.ihvp_calculator._compute_ihvp_single_batch( # pylint: disable=protected-access + tf.expand_dims(v, axis=0), + use_gradient=False + ), grads - ) # pylint: disable=protected-access + ) second_term = tf.reduce_sum(tf.reshape(second_term, tf.shape(grads)), axis=1) # Second, we compute the first term, which contains the weights - first_term = tf.concat([w for w in weights], axis=0) + first_term = tf.concat(list(weights), axis=0) first_term = tf.multiply( first_term, tf.repeat( @@ -302,7 +308,7 @@ def _estimate_influence_value_from_influence_vector( A tensor with influence values for the (batch of) test samples. """ # Extract the different information inside the tuples - feature_maps_test, labels_test = preproc_test_sample + feature_maps_test, _ = preproc_test_sample alpha, feature_maps_train = influence_vector if len(alpha.shape) == 1 or (len(alpha.shape) == 2 and alpha.shape[1] == 1): diff --git a/deel/influenciae/utils/tf_operations.py b/deel/influenciae/utils/tf_operations.py index 0d5d896..5f98392 100644 --- a/deel/influenciae/utils/tf_operations.py +++ b/deel/influenciae/utils/tf_operations.py @@ -8,7 +8,6 @@ import numpy as np import tensorflow as tf -from tensorflow.python.framework.convert_to_constants import convert_variables_to_constants_v2 from ..types import Union, Tuple, Optional, Callable From c28abb6750443191f4d8f96b7877602da99fae77 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Mon, 12 Feb 2024 18:42:54 +0100 Subject: [PATCH 16/26] rps: create a base interface for representer point calculators + adapt l2 and LJE --- deel/influenciae/rps/__init__.py | 3 +- .../influenciae/rps/base_representer_point.py | 214 ++++++++++++++++++ deel/influenciae/rps/rps_l2.py | 151 +----------- deel/influenciae/rps/rps_lje.py | 28 +-- 4 files changed, 231 insertions(+), 165 deletions(-) create mode 100644 deel/influenciae/rps/base_representer_point.py diff --git a/deel/influenciae/rps/__init__.py b/deel/influenciae/rps/__init__.py index 20365e2..2873541 100644 --- a/deel/influenciae/rps/__init__.py +++ b/deel/influenciae/rps/__init__.py @@ -3,8 +3,9 @@ # CRIAQ and ANITI - https://www.deel.ai/ # ===================================================================================== """ -Representer Point L2 module +Representer Point theorem module """ +from .base_representer_point import BaseRepresenterPoint from .rps_l2 import RepresenterPointL2 from .rps_lje import RepresenterPointLJE diff --git a/deel/influenciae/rps/base_representer_point.py b/deel/influenciae/rps/base_representer_point.py new file mode 100644 index 0000000..27e0e57 --- /dev/null +++ b/deel/influenciae/rps/base_representer_point.py @@ -0,0 +1,214 @@ +# Copyright IRT Antoine de Saint Exupéry et Université Paul Sabatier Toulouse III - All +# rights reserved. DEEL is a research program operated by IVADO, IRT Saint Exupéry, +# CRIAQ and ANITI - https://www.deel.ai/ +# ===================================================================================== +""" +Module containing the base class for representer point theorem-based influence calculators +""" +from abc import abstractmethod + +import tensorflow as tf +from tensorflow.keras import Model +from tensorflow.keras.losses import Loss, Reduction + +from ..common import BaseInfluenceCalculator +from ..types import Tuple, Callable, Union + +from ..utils import assert_batched_dataset, split_model + + +class BaseRepresenterPoint(BaseInfluenceCalculator): + """ + Base interface for representer point theorem-based influence calculators. + + Disclaimer: This method only works on classification problems! + + Parameters + ---------- + model + A TF2 model that has already been trained + train_set + A batched TF dataset with the points with which the model was trained + loss_function + The loss function with which the model was trained. This loss function MUST NOT be reduced. + """ + def __init__( + self, + model: Model, + train_set: tf.data.Dataset, + loss_function: Union[Callable[[tf.Tensor, tf.Tensor], tf.Tensor], Loss], + target_layer: Union[str, int] = -1 + ): + # Make sure that the dataset is batched and that the loss function is not reduced + assert_batched_dataset(train_set) + self.train_set = train_set + if hasattr(loss_function, 'reduction'): + assert loss_function.reduction == Reduction.NONE + + # Make sure that the model's last layer is a Dense layer with no bias + if not isinstance(model.layers[-1], tf.keras.layers.Dense): + raise ValueError('The last layer of the model must be a Dense layer with no bias.') + if model.layers[-1].use_bias: + raise ValueError('The last layer of the model must be a Dense layer with no bias.') + self.loss_function = loss_function + + # Cut the model in two (feature extractor and head) + self.model = model + self.target_layer = target_layer + self.feature_extractor, self.original_head = split_model(model, target_layer) + + @abstractmethod + def _compute_alpha(self, z_batch: tf.Tensor, y_batch: tf.Tensor) -> tf.Tensor: + """ + Compute the alpha vector for a given input-output pair (z, y) + + Parameters + ---------- + z_batch + A tensor containing the latent representation of an input point. + y_batch + The labels corresponding to the representations z + + Returns + ------- + alpha + A tensor with the alpha coefficients of the kernel given by the representer point theorem + """ + raise NotImplementedError() + + def _preprocess_samples(self, samples: Tuple[tf.Tensor, ...]) -> tf.Tensor: + """ + Preprocess a single batch of samples. + + Parameters + ---------- + samples + A single batch of tensors containing the samples. + + Returns + ------- + evaluate_vect + The preprocessed sample + """ + x_batch = self.feature_extractor(samples[:-1]) + y_t = samples[-1] + + return x_batch, y_t + + def _compute_influence_vector(self, train_samples: Tuple[tf.Tensor, ...]) -> tf.Tensor: + """ + Compute an equivalent of the influence vector for a sample of training points. + + Disclaimer: this vector is not an estimation of the difference between the actual + model and the perturbed model without the samples (like it is the case with what is + calculated using deel.influenciae.influence). + + Parameters + ---------- + train_samples + A tensor with a group of training samples of which we wish to compute the influence. + + Returns + ------- + influence_vectors + A tensor with a concatenation of the alpha weights and the feature maps for each sample. + This allows for optimizations to be put in place but is not really an influence vector + of any kind. + """ + x_batch = self.feature_extractor(train_samples[:-1]) + alpha = self._compute_alpha(x_batch, train_samples[-1]) + + return alpha, x_batch + + def _estimate_individual_influence_values_from_batch( + self, + train_samples: Tuple[tf.Tensor, ...], + samples_to_evaluate: Tuple[tf.Tensor, ...] + ) -> tf.Tensor: + """ + Estimate the (individual) influence scores of a single batch of samples with respect to + a batch of samples belonging to the model's training dataset. + + Parameters + ---------- + train_samples + A single batch of training samples (and their target values). + samples_to_evaluate + A single batch of samples of which we wish to compute the influence of removing the training + samples. + + Returns + ------- + A tensor containing the individual influence scores. + """ + return self._estimate_influence_value_from_influence_vector( + self._preprocess_samples(samples_to_evaluate), + self._compute_influence_vector(train_samples) + ) + + def _estimate_influence_value_from_influence_vector( + self, + preproc_test_sample: tf.Tensor, + influence_vector: tf.Tensor + ) -> tf.Tensor: + """ + Compute the influence score for a (batch of) preprocessed test sample(s) and a training "influence vector". + + Parameters + ---------- + preproc_test_sample + A tensor with a pre-processed sample to evaluate. + influence_vector + A tensor with the training influence vector. + + Returns + ------- + influence_values + A tensor with influence values for the (batch of) test samples. + """ + # Extract the different information inside the tuples + feature_maps_test, _ = preproc_test_sample + alpha, feature_maps_train = influence_vector + + if len(alpha.shape) == 1 or (len(alpha.shape) == 2 and alpha.shape[1] == 1): + influence_values = alpha * tf.matmul(feature_maps_train, feature_maps_test, transpose_b=True) + else: + influence_values = tf.gather( + alpha, tf.argmax(self.original_head(feature_maps_test), axis=1), axis=1, batch_dims=1 + ) * tf.matmul(feature_maps_train, feature_maps_test, transpose_b=True) + influence_values = tf.transpose(influence_values) + + return influence_values + + def _compute_influence_value_from_batch(self, train_samples: Tuple[tf.Tensor, ...]) -> tf.Tensor: + """ + Compute the influence score for a batch of training samples (i.e. self-influence). + + Parameters + ---------- + train_samples + A tensor containing a batch of training samples. + + Returns + ------- + influence_values + A tensor with the self-influence of the training samples. + """ + x_batch = self.feature_extractor(train_samples[:-1]) + alpha = self._compute_alpha(x_batch, train_samples[-1]) + + # If the problem is binary classification, take all the alpha values + # If multiclass, take only those that correspond to the prediction + out_shape = self.model.output_shape + if len(out_shape) == 1: + influence_values = alpha + elif len(out_shape) == 2 and out_shape[1] == 1: + influence_values = alpha + else: + if len(out_shape) > 2: + indices = tf.argmax(tf.squeeze(self.original_head(x_batch), axis=-1), axis=1) + else: + indices = tf.argmax(self.original_head(x_batch), axis=1) + influence_values = tf.gather(alpha, indices, axis=1, batch_dims=1) + + return tf.abs(influence_values) diff --git a/deel/influenciae/rps/rps_l2.py b/deel/influenciae/rps/rps_l2.py index b3fb5e3..5aa805d 100644 --- a/deel/influenciae/rps/rps_l2.py +++ b/deel/influenciae/rps/rps_l2.py @@ -13,13 +13,13 @@ from tensorflow.keras.losses import MeanSquaredError, Loss, Reduction #pylint: disable=E0611 from tensorflow.keras.regularizers import L2 #pylint: disable=E0611 -from ..common import BaseInfluenceCalculator +from . import BaseRepresenterPoint from ..types import Tuple, Callable, Union -from ..utils import assert_batched_dataset, BacktrackingLineSearch, dataset_size +from ..utils import BacktrackingLineSearch, dataset_size -class RepresenterPointL2(BaseInfluenceCalculator): +class RepresenterPointL2(BaseRepresenterPoint): """ A class implementing a method to compute the influence of training points through the representer point theorem for kernels. @@ -59,15 +59,10 @@ def __init__( lambda_regularization: float, scaling_factor: float = 0.1, epochs: int = 100, - layer_index: int = -2, + layer_index: int = -1, ): - assert_batched_dataset(train_set) - if hasattr(loss_function, 'reduction'): - assert loss_function.reduction == Reduction.NONE - self.loss_function = loss_function + super().__init__(model, train_set, loss_function, layer_index) self.n_train = dataset_size(train_set) - self.feature_extractor = Model(inputs=model.input, outputs=model.layers[layer_index].output) - self.model = model self.train_set = train_set self.lambda_regularization = lambda_regularization self.scaling_factor = scaling_factor @@ -75,142 +70,6 @@ def __init__( self.linear_layer = None self._train_last_layer(self.epochs) - def _compute_influence_vector(self, train_samples: Tuple[tf.Tensor, ...]) -> tf.Tensor: - """ - Compute an equivalent of the influence vector for a sample of training points. - - Disclaimer: this vector is not an estimation of the difference between the actual - model and the perturbed model without the samples (like it is the case with what is - calculated using deel.influenciae.influence). - - Parameters - ---------- - train_samples - A tensor with a group of training samples of which we wish to compute the influence. - - Returns - ------- - influence_vectors - A tensor with a concatenation of the alpha weights and the feature maps for each sample. - This allows for optimizations to be put in place but is not really an influence vector - of any kind. - """ - x_batch = self.feature_extractor(train_samples[:-1]) - alpha = self._compute_alpha(x_batch, train_samples[-1]) - - return alpha, x_batch - - def _preprocess_samples(self, samples: Tuple[tf.Tensor, ...]) -> tf.Tensor: - """ - Preprocess a single batch of samples. - - Parameters - ---------- - samples - A single batch of tensors containing the samples. - - Returns - ------- - evaluate_vect - The preprocessed sample - """ - x_batch = self.feature_extractor(samples[:-1]) - y_t = tf.argmax(self.model(samples[:-1]), axis=1) - - return x_batch, y_t - - def _estimate_individual_influence_values_from_batch( - self, - train_samples: Tuple[tf.Tensor, ...], - samples_to_evaluate: Tuple[tf.Tensor, ...] - ) -> tf.Tensor: - """ - Estimate the (individual) influence scores of a single batch of samples with respect to - a batch of samples belonging to the model's training dataset. - - Parameters - ---------- - train_samples - A single batch of training samples (and their target values). - samples_to_evaluate - A single batch of samples of which we wish to compute the influence of removing the training - samples. - - Returns - ------- - A tensor containing the individual influence scores. - """ - return self._estimate_influence_value_from_influence_vector( - self._preprocess_samples(samples_to_evaluate), - self._compute_influence_vector(train_samples) - ) - - def _estimate_influence_value_from_influence_vector( - self, - preproc_test_sample: tf.Tensor, - influence_vector: tf.Tensor - ) -> tf.Tensor: - """ - Compute the influence score for a (batch of) preprocessed test sample(s) and a training "influence vector". - - Parameters - ---------- - preproc_test_sample - A tensor with a pre-processed sample to evaluate. - influence_vector - A tensor with the training influence vector. - - Returns - ------- - influence_values - A tensor with influence values for the (batch of) test samples. - """ - # Extract the different information inside the tuples - feature_maps_test, labels_test = preproc_test_sample - alpha, feature_maps_train = influence_vector - - if len(alpha.shape) == 1 or (len(alpha.shape) == 2 and alpha.shape[1] == 1): - influence_values = alpha * tf.matmul(feature_maps_train, feature_maps_test, transpose_b=True) - else: - influence_values = tf.gather(alpha, labels_test, axis=1, batch_dims=1) * \ - tf.matmul(feature_maps_train, feature_maps_test, transpose_b=True) - influence_values = tf.transpose(influence_values) - - return influence_values - - def _compute_influence_value_from_batch(self, train_samples: Tuple[tf.Tensor, ...]) -> tf.Tensor: - """ - Compute the influence score for a batch of training samples (i.e. self-influence). - - Parameters - ---------- - train_samples - A tensor containing a batch of training samples. - - Returns - ------- - influence_values - A tensor with the self-influence of the training samples. - """ - x_batch = self.feature_extractor(train_samples[:-1]) - alpha = self._compute_alpha(x_batch, train_samples[-1]) - - # If the problem is binary classification, take all the alpha values - # If multiclass, take only those that correspond to the prediction - out_shape = self.model.output_shape - if len(out_shape) == 1: - influence_values = alpha - elif len(out_shape) == 2 and out_shape[1] == 1: - influence_values = alpha - else: - if len(out_shape) > 2: - indices = tf.argmax(tf.squeeze(self.model(train_samples[:-1]), axis=-1), axis=1) - else: - indices = tf.argmax(self.model(train_samples[:-1]), axis=1) - influence_values = tf.gather(alpha, indices, axis=1, batch_dims=1) - - return tf.abs(influence_values) - def _train_last_layer(self, epochs: int): """ Trains an L2-regularized surrogate linear model to predict like the model on the diff --git a/deel/influenciae/rps/rps_lje.py b/deel/influenciae/rps/rps_lje.py index 0db673b..aaf4a6c 100644 --- a/deel/influenciae/rps/rps_lje.py +++ b/deel/influenciae/rps/rps_lje.py @@ -11,12 +11,13 @@ import tensorflow as tf -from ..common import InfluenceModel, InverseHessianVectorProductFactory, BaseInfluenceCalculator -from ..utils import map_to_device, split_model, assert_batched_dataset +from . import BaseRepresenterPoint +from ..common import InfluenceModel, InverseHessianVectorProductFactory +from ..utils import map_to_device from ..types import Union, Optional -class RepresenterPointLJE(BaseInfluenceCalculator): +class RepresenterPointLJE(BaseRepresenterPoint): """ Representer Point Selection via Local Jacobian Expansion for Post-hoc Classifier Explanation of Deep Neural Networks and Ensemble Models @@ -52,33 +53,25 @@ def __init__( shuffle_buffer_size: int = 10000, epsilon: float = 1e-5 ): - # Make sure that the model's last layer is a Dense layer with no bias - if not isinstance(influence_model.model.layers[-1], tf.keras.layers.Dense): - raise ValueError('The last layer of the model must be a Dense layer with no bias.') - if influence_model.model.layers[-1].use_bias: - raise ValueError('The last layer of the model must be a Dense layer with no bias.') - - # Make sure that the dataset is batched - assert_batched_dataset(dataset) - - self.target_layer = target_layer + super().__init__(influence_model.model, dataset, influence_model.loss_function) self.epsilon = tf.constant(epsilon, dtype=tf.float32) # In the paper, the authors explain that in practice, they use a single step of SGD to compute the # perturbed model's weights. We will do the same here. optimizer = tf.keras.optimizers.SGD(learning_rate=1e-4) - feature_extractor, perturbed_head = split_model(influence_model.model, target_layer) target_layer_shape = influence_model.model.layers[target_layer].input.type_spec.shape + perturbed_head = tf.keras.models.clone_model(self.original_head) + perturbed_head.set_weights(self.original_head.get_weights()) perturbed_head.build(target_layer_shape) perturbed_head.compile(optimizer=optimizer, loss=influence_model.loss_function) # Get a dataset to compute the SGD step if n_samples_for_hessian is None: - dataset_to_estimate_hessian = map_to_device(dataset, lambda x, y: (feature_extractor(x), y)) + dataset_to_estimate_hessian = map_to_device(dataset, lambda x, y: (self.feature_extractor(x), y)) else: dataset_to_estimate_hessian = map_to_device( dataset.shuffle(shuffle_buffer_size).take(n_samples_for_hessian), - lambda x, y: (feature_extractor(x), y) + lambda x, y: (self.feature_extractor(x), y) ) # Accumulate the gradients for the whole dataset and then update @@ -93,8 +86,7 @@ def __init__( _ = [accum_vars[i].assign_add(grad) for i, grad in enumerate(gradients)] optimizer.apply_gradients(zip(accum_vars, trainable_vars)) - # Keep the feature extractor and the perturbed head - self.feature_extractor = feature_extractor + # Keep the perturbed head self.perturbed_head = perturbed_head # Create the new model with the perturbed weights to compute the hessian matrix From cf3ab830f5d1cbc965b38b45a2f9782800c6e4c7 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Mon, 12 Feb 2024 18:43:32 +0100 Subject: [PATCH 17/26] test_rps_l2: adapt the test to verify the no bias constraint --- tests/rps/test_representer_point_l2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/rps/test_representer_point_l2.py b/tests/rps/test_representer_point_l2.py index f719f14..3410c6c 100644 --- a/tests/rps/test_representer_point_l2.py +++ b/tests/rps/test_representer_point_l2.py @@ -19,7 +19,7 @@ def test_surrogate_model(): tf.keras.layers.Input(shape=(32, 32, 3)), tf.keras.layers.Conv2D(16, 3, 4, "same", activation='swish'), tf.keras.layers.GlobalAveragePooling2D(), - tf.keras.layers.Dense(4) + tf.keras.layers.Dense(4, use_bias=False) ]) model.compile( optimizer=tf.keras.optimizers.Adam(1e-2), @@ -54,7 +54,7 @@ def test_gradients(): tf.keras.layers.Input(shape=(32, 32, 3)), tf.keras.layers.Conv2D(16, 3, 4, "valid", activation='relu'), tf.keras.layers.GlobalAveragePooling2D(), - tf.keras.layers.Dense(4) + tf.keras.layers.Dense(4, use_bias=False) ]) model.compile( optimizer=tf.keras.optimizers.Adam(1e-2), @@ -107,7 +107,7 @@ def test_influence_values(): tf.keras.layers.Input(shape=(32, 32, 3)), tf.keras.layers.Conv2D(16, 3, 4, "valid", activation='relu'), tf.keras.layers.GlobalAveragePooling2D(), - tf.keras.layers.Dense(4) + tf.keras.layers.Dense(4, use_bias=False) ]) model.compile( optimizer=tf.keras.optimizers.Adam(1e-2), @@ -157,7 +157,7 @@ def test_predict_with_kernel(): tf.keras.layers.Input(shape=(32, 32, 3)), tf.keras.layers.Conv2D(16, 3, 5, "valid", activation='relu'), tf.keras.layers.GlobalAveragePooling2D(), - tf.keras.layers.Dense(1) + tf.keras.layers.Dense(1, use_bias=False) ]) model.compile( optimizer=tf.keras.optimizers.Adam(1e-2), From aece23905c089132ff500296066c1e2ccc7f6811 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Tue, 13 Feb 2024 10:23:06 +0100 Subject: [PATCH 18/26] rps_lje: adapt the rps lje class to the new rps interface --- deel/influenciae/rps/rps_lje.py | 162 -------------------------------- 1 file changed, 162 deletions(-) diff --git a/deel/influenciae/rps/rps_lje.py b/deel/influenciae/rps/rps_lje.py index aaf4a6c..a700e6d 100644 --- a/deel/influenciae/rps/rps_lje.py +++ b/deel/influenciae/rps/rps_lje.py @@ -7,8 +7,6 @@ but using a local jacobian expansion, as per https://proceedings.neurips.cc/paper/2021/file/c460dc0f18fc309ac07306a4a55d2fd6-Paper.pdf """ -from typing import Tuple - import tensorflow as tf from . import BaseRepresenterPoint @@ -97,45 +95,6 @@ def __init__( ) self.ihvp_calculator = ihvp_calculator_factory.build(model, dataset_to_estimate_hessian) - def _reshape_assign(self, weights, influence_vector: tf.Tensor) -> None: - """ - Updates the model's weights in-place for the Local Jacobian Expansion approximation. - - Parameters - ---------- - weights - The weights for which we wish to compute the influence-related quantities. - influence_vector - A tensor with the optimizer's stepped weights. - """ - index = 0 - for w in weights: - shape = tf.shape(w) - size = tf.reduce_prod(shape) - v = influence_vector[index:(index + size)] - index += size - v = tf.reshape(v, shape) - w.assign(w - v) - - def _preprocess_samples(self, samples: Tuple[tf.Tensor, ...]) -> tf.Tensor: - """ - Preprocess a single batch of samples. - - Parameters - ---------- - samples - A single batch of tensors containing the samples. - - Returns - ------- - evaluate_vect - The preprocessed sample - """ - z_batch = self.feature_extractor(samples[:-1]) - y_batch = samples[-1] - - return z_batch, y_batch - def _compute_alpha(self, z_batch: tf.Tensor, y_batch: tf.Tensor) -> tf.Tensor: """ Computes the alpha vector for the Local Jacobian Expansion approximation. @@ -194,124 +153,3 @@ def _compute_alpha(self, z_batch: tf.Tensor, y_batch: tf.Tensor) -> tf.Tensor: first_term = tf.reduce_sum(first_term, axis=1) return first_term - second_term # alpha is first term minus second term - - def _compute_influence_value_from_batch(self, train_samples: Tuple[tf.Tensor, ...]) -> tf.Tensor: - """ - Compute the influence score for a batch of training samples (i.e. self-influence). - - Parameters - ---------- - train_samples - A tensor containing a batch of training samples. - - Returns - ------- - influence_values - A tensor with the self-influence of the training samples. - """ - z_batch, y_batch = self._preprocess_samples(train_samples) - alpha = self._compute_alpha(z_batch, y_batch) - - # If the problem is binary classification, take all the alpha values - # If multiclass, take only those that correspond to the prediction - out_shape = self.perturbed_head.output_shape - if len(out_shape) == 1: - influence_values = alpha - elif len(out_shape) == 2 and out_shape[1] == 1: - influence_values = alpha - else: - if len(out_shape) > 2: - indices = tf.argmax(tf.squeeze(self.perturbed_head(z_batch), axis=-1), axis=1) - else: - indices = tf.argmax(self.perturbed_head(z_batch), axis=1) - influence_values = tf.gather(alpha, indices, axis=1, batch_dims=1) - - return influence_values - - def _compute_influence_vector(self, train_samples: Tuple[tf.Tensor, ...]) -> tf.Tensor: - """ - Compute an equivalent of the influence vector for a sample of training points. - - Disclaimer: this vector is not an estimation of the difference between the actual - model and the perturbed model without the samples (like it is the case with what is - calculated using deel.influenciae.influence). - - Parameters - ---------- - train_samples - A tensor with a group of training samples of which we wish to compute the influence. - - Returns - ------- - influence_vectors - A tensor with a concatenation of the alpha weights and the feature maps for each sample. - This allows for optimizations to be put in place but is not really an influence vector - of any kind. - """ - z_batch = self.feature_extractor(train_samples[:-1]) - alpha = self._compute_alpha(z_batch, train_samples[-1]) - - return alpha, z_batch - - def _estimate_individual_influence_values_from_batch( - self, - train_samples: Tuple[tf.Tensor, ...], - samples_to_evaluate: Tuple[tf.Tensor, ...] - ) -> tf.Tensor: - """ - Estimate the (individual) influence scores of a single batch of samples with respect to - a batch of samples belonging to the model's training dataset. - - Parameters - ---------- - train_samples - A single batch of training samples (and their target values). - samples_to_evaluate - A single batch of samples of which we wish to compute the influence of removing the training - samples. - - Returns - ------- - A tensor containing the individual influence scores. - """ - return self._estimate_influence_value_from_influence_vector( - self._preprocess_samples(samples_to_evaluate), - self._compute_influence_vector(train_samples) - ) - - def _estimate_influence_value_from_influence_vector( - self, - preproc_test_sample: tf.Tensor, - influence_vector: tf.Tensor - ) -> tf.Tensor: - """ - Compute the influence score for a (batch of) preprocessed test sample(s) and a training "influence vector". - - Parameters - ---------- - preproc_test_sample - A tensor with a pre-processed sample to evaluate. - influence_vector - A tensor with the training influence vector. - - Returns - ------- - influence_values - A tensor with influence values for the (batch of) test samples. - """ - # Extract the different information inside the tuples - feature_maps_test, _ = preproc_test_sample - alpha, feature_maps_train = influence_vector - - if len(alpha.shape) == 1 or (len(alpha.shape) == 2 and alpha.shape[1] == 1): - influence_values = alpha * tf.matmul(feature_maps_train, feature_maps_test, transpose_b=True) - else: - if len(self.perturbed_head.output_shape) > 2: - indices = tf.argmax(tf.squeeze(self.perturbed_head(feature_maps_test), axis=-1), axis=1) - else: - indices = tf.argmax(self.perturbed_head(feature_maps_test), axis=1) - influence_values = tf.gather(alpha, indices, axis=1, batch_dims=1) * \ - tf.matmul(feature_maps_train, feature_maps_test, transpose_b=True) - influence_values = tf.transpose(influence_values) - - return influence_values From 7733394465b128deff8134711cff1b0335fcf5ab Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Tue, 13 Feb 2024 10:23:42 +0100 Subject: [PATCH 19/26] test_rps_lje: fix the tests --- tests/rps/test_rps_lje.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/rps/test_rps_lje.py b/tests/rps/test_rps_lje.py index 443b1d3..61268e9 100644 --- a/tests/rps/test_rps_lje.py +++ b/tests/rps/test_rps_lje.py @@ -197,7 +197,7 @@ def test_compute_influence_value_from_influence_vector_binary(): # Compute the influence values manually alpha, z_batch = rps_lje._compute_influence_vector((inputs_train, targets_train)) # already checked in another test - influence_values = alpha + influence_values = tf.abs(alpha) assert almost_equal(influence_values_computed, influence_values, epsilon=1e-3) @@ -228,9 +228,9 @@ def test_compute_influence_value_from_influence_vector_multiclass(): # Compute the influence values manually alpha, z_batch = rps_lje._compute_influence_vector((inputs_train, targets_train)) # already checked in another test alpha_i = tf.gather(alpha, tf.argmax(rps_lje.perturbed_head(z_batch), axis=1), axis=1, batch_dims=1) - influence_values = alpha_i + influence_values = tf.abs(alpha_i) - assert almost_equal(influence_values_computed, influence_values, epsilon=1e-3) + assert relative_almost_equal(influence_values_computed, influence_values, percent=0.05) def test_compute_pairwise_influence_value_binary(): From 31ec52756eec7386f65ac6927c4a8e4f8543b03b Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Tue, 13 Feb 2024 11:33:43 +0100 Subject: [PATCH 20/26] lint: clean-up --- deel/influenciae/rps/rps_l2.py | 4 ++-- deel/influenciae/rps/rps_lje.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deel/influenciae/rps/rps_l2.py b/deel/influenciae/rps/rps_l2.py index 5aa805d..4f0d786 100644 --- a/deel/influenciae/rps/rps_l2.py +++ b/deel/influenciae/rps/rps_l2.py @@ -10,10 +10,10 @@ import tensorflow as tf from tensorflow.keras import Model #pylint: disable=E0611 from tensorflow.keras.layers import Input, Dense #pylint: disable=E0611 -from tensorflow.keras.losses import MeanSquaredError, Loss, Reduction #pylint: disable=E0611 +from tensorflow.keras.losses import MeanSquaredError, Loss #pylint: disable=E0611 from tensorflow.keras.regularizers import L2 #pylint: disable=E0611 -from . import BaseRepresenterPoint +from .base_representer_point import BaseRepresenterPoint from ..types import Tuple, Callable, Union from ..utils import BacktrackingLineSearch, dataset_size diff --git a/deel/influenciae/rps/rps_lje.py b/deel/influenciae/rps/rps_lje.py index a700e6d..ec3f51e 100644 --- a/deel/influenciae/rps/rps_lje.py +++ b/deel/influenciae/rps/rps_lje.py @@ -9,7 +9,7 @@ """ import tensorflow as tf -from . import BaseRepresenterPoint +from .base_representer_point import BaseRepresenterPoint from ..common import InfluenceModel, InverseHessianVectorProductFactory from ..utils import map_to_device from ..types import Union, Optional From 727e5e4f0f406b499b399a5cbbeb9392d292513e Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Fri, 16 Feb 2024 12:16:15 +0100 Subject: [PATCH 21/26] rps lje: move feature extractor outside of map as it was causing problems in benchmark --- deel/influenciae/rps/rps_lje.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/deel/influenciae/rps/rps_lje.py b/deel/influenciae/rps/rps_lje.py index ec3f51e..e929cee 100644 --- a/deel/influenciae/rps/rps_lje.py +++ b/deel/influenciae/rps/rps_lje.py @@ -11,7 +11,6 @@ from .base_representer_point import BaseRepresenterPoint from ..common import InfluenceModel, InverseHessianVectorProductFactory -from ..utils import map_to_device from ..types import Union, Optional @@ -65,12 +64,16 @@ def __init__( # Get a dataset to compute the SGD step if n_samples_for_hessian is None: - dataset_to_estimate_hessian = map_to_device(dataset, lambda x, y: (self.feature_extractor(x), y)) + dataset_to_estimate_hessian = dataset else: - dataset_to_estimate_hessian = map_to_device( - dataset.shuffle(shuffle_buffer_size).take(n_samples_for_hessian), - lambda x, y: (self.feature_extractor(x), y) - ) + n_batches_for_hessian = max(n_samples_for_hessian // dataset._batch_size, 1) + dataset_to_estimate_hessian = dataset.shuffle(shuffle_buffer_size).take(n_batches_for_hessian) + f_array, y_array = None, None + for x, y in dataset_to_estimate_hessian: + f = self.feature_extractor(x) + f_array = f if f_array is None else tf.concat([f_array, f], axis=0) + y_array = y if y_array is None else tf.concat([y_array, y], axis=0) + dataset_to_estimate_hessian = tf.data.Dataset.from_tensor_slices((f_array, y_array)).batch(dataset._batch_size) # Accumulate the gradients for the whole dataset and then update trainable_vars = perturbed_head.trainable_variables From f1a360ffd8f9c929a72f7566b1abff347db2a705 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Fri, 16 Feb 2024 12:39:51 +0100 Subject: [PATCH 22/26] base influence: remove duplicate method --- deel/influenciae/common/base_influence.py | 24 ----------------------- 1 file changed, 24 deletions(-) diff --git a/deel/influenciae/common/base_influence.py b/deel/influenciae/common/base_influence.py index 5164e75..7484661 100644 --- a/deel/influenciae/common/base_influence.py +++ b/deel/influenciae/common/base_influence.py @@ -269,30 +269,6 @@ def compute_influence_vector( return inf_vect_ds - @abstractmethod - def _estimate_individual_influence_values_from_batch( - self, - train_samples: Tuple[tf.Tensor, ...], - samples_to_evaluate: Tuple[tf.Tensor, ...] - ) -> tf.Tensor: - """ - Estimate the (individual) influence scores of a single batch of samples with respect to - a batch of samples belonging to the model's training dataset. - - Parameters - ---------- - train_samples - A single batch of training samples (and their target values). - samples_to_evaluate - A single batch of samples of which we wish to compute the influence of removing the training - samples. - - Returns - ------- - A tensor containing the individual influence scores. - """ - raise NotImplementedError() - def estimate_influence_values_in_batches( self, dataset_to_evaluate: tf.data.Dataset, From 07fbb55474d4db8292eebb5a6ed77389d5033cc6 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Fri, 16 Feb 2024 18:12:12 +0100 Subject: [PATCH 23/26] bench tests: update use_bias parameter for RPS calculators --- tests/benchmark/test_bench.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/benchmark/test_bench.py b/tests/benchmark/test_bench.py index 0cb49ee..4037bc7 100644 --- a/tests/benchmark/test_bench.py +++ b/tests/benchmark/test_bench.py @@ -107,7 +107,8 @@ def test_rps_lje(): test_batch_size=10, epochs_to_save=None, take_batch=take_batch, - verbose_training=False) + verbose_training=False, + use_bias=False) influence_factory = RPSLJEFactory('exact') result = cifar10_evaluator.evaluate(influence_factory=influence_factory, nbr_of_evaluation=2, verbose=False) @@ -126,7 +127,8 @@ def test_rps_l2(): test_batch_size=10, epochs_to_save=None, take_batch=take_batch, - verbose_training=False) + verbose_training=False, + use_bias=False) influence_factory = RPSL2Factory(CategoricalCrossentropy(from_logits=True, reduction=Reduction.NONE), lambda_regularization=10.0) From a930df2a8937d5471aefde27bd301b8cee798d79 Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Thu, 22 Feb 2024 17:14:20 +0100 Subject: [PATCH 24/26] readme: add the bibtex citation of the pre-print --- README.md | 19 +++++++++++++++++++ docs/index.md | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/README.md b/README.md index f35ac7f..c98b7af 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,25 @@ This project received funding from the French ”Investing for the Future – PI This library was first created as a research tool by [Agustin Martin PICARD](mailto:agustin-martin.picard@irt-saintexupery.com) in the context of the DEEL project with the help of [David Vigouroux](mailto:david.vigouroux@irt-saintexupery.com) and [Thomas FEL](http://thomasfel.fr). Later on, [Lucas Hervier](https://github.com/lucashervier) joined the team to transform the code base as a practical user-(almost)-friendly and efficient tool. +## 🗞️ Citation + +If you use Influenciae as part of your workflow in a scientific publication, please consider citing the 🗞️ [official paper](https://hal.science/hal-04284178/): + +``` +@unpublished{picard:hal-04284178, + TITLE = {{Influenci{\ae}: A library for tracing the influence back to the data-points}}, + AUTHOR = {Picard, Agustin Martin and Hervier, Lucas and Fel, Thomas and Vigouroux, David}, + URL = {https://hal.science/hal-04284178}, + NOTE = {working paper or preprint}, + YEAR = {2023}, + MONTH = Nov, + KEYWORDS = {Data-centric ai ; XAI ; Explainability ; Influence Functions ; Open-source toolbox}, + PDF = {https://hal.science/hal-04284178/file/ms.pdf}, + HAL_ID = {hal-04284178}, + HAL_VERSION = {v1}, +} +``` + ## 📝 License The package is released under MIT license. diff --git a/docs/index.md b/docs/index.md index 8b78804..7bdcf7f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -172,6 +172,25 @@ This project received funding from the French ”Investing for the Future – PI This library was first created as a research tool by [Agustin Martin PICARD](mailto:agustin-martin.picard@irt-saintexupery.com) in the context of the DEEL project with the help of [David Vigouroux](mailto:david.vigouroux@irt-saintexupery.com) and [Thomas FEL](http://thomasfel.fr). Later on, [Lucas Hervier](https://github.com/lucashervier) joined the team to transform (at least attempt) the code base as a practical user-(almost)-friendly and efficient tool. +## 🗞️ Citation + +If you use Influenciae as part of your workflow in a scientific publication, please consider citing the 🗞️ [official paper](https://hal.science/hal-04284178/): + +``` +@unpublished{picard:hal-04284178, + TITLE = {{Influenci{\ae}: A library for tracing the influence back to the data-points}}, + AUTHOR = {Picard, Agustin Martin and Hervier, Lucas and Fel, Thomas and Vigouroux, David}, + URL = {https://hal.science/hal-04284178}, + NOTE = {working paper or preprint}, + YEAR = {2023}, + MONTH = Nov, + KEYWORDS = {Data-centric ai ; XAI ; Explainability ; Influence Functions ; Open-source toolbox}, + PDF = {https://hal.science/hal-04284178/file/ms.pdf}, + HAL_ID = {hal-04284178}, + HAL_VERSION = {v1}, +} +``` + ## 📝 License The package is released under MIT license. From 23ac7e47cc44d344fdf680f200a22749e0e4d9ee Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Tue, 9 Apr 2024 17:23:51 +0200 Subject: [PATCH 25/26] ci: update the bumpversion config --- .bumpversion.cfg | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 5b59266..9585b8b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,18 +1,7 @@ [bumpversion] current_version = 0.2.0 -parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P\d+))? -serialize = - {major}.{minor}.{patch}-{prerelease} - {major}.{minor}.{patch} commit = True -tag = True - -[bumpversion:part:prerelease] -optional_value = regular -values = - beta - alpha - regular +tag = False [bumpversion:file:setup.py] From bfccb37500ee1dd763100827fdbd4554874d196f Mon Sep 17 00:00:00 2001 From: "agustin-martin.picard" Date: Tue, 9 Apr 2024 17:24:08 +0200 Subject: [PATCH 26/26] =?UTF-8?q?Bump=20version:=200.2.0=20=E2=86=92=200.3?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- deel/influenciae/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9585b8b..ef4bef6 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.2.0 +current_version = 0.3.0 commit = True tag = False diff --git a/deel/influenciae/__init__.py b/deel/influenciae/__init__.py index dc8535d..7079315 100644 --- a/deel/influenciae/__init__.py +++ b/deel/influenciae/__init__.py @@ -10,7 +10,7 @@ techniques """ -__version__ = '0.2.0' +__version__ = '0.3.0' from . import influence from . import common diff --git a/setup.py b/setup.py index f25d576..3c5a220 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="Influenciae", - version="0.2.0", + version="0.3.0", description="A Tensorflow Toolbox for Influence Functions", long_description=README, long_description_content_type="text/markdown",