diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 5b59266..ef4bef6 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} +current_version = 0.3.0 commit = True -tag = True - -[bumpversion:part:prerelease] -optional_value = regular -values = - beta - alpha - regular +tag = False [bumpversion:file:setup.py] diff --git a/README.md b/README.md index 9805dda..c98b7af 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 @@ -170,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/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/__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/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 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, 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() 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() 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, 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/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/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..4f0d786 100644 --- a/deel/influenciae/rps/rps_l2.py +++ b/deel/influenciae/rps/rps_l2.py @@ -10,16 +10,16 @@ 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 ..common import BaseInfluenceCalculator +from .base_representer_point 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 814084e..e929cee 100644 --- a/deel/influenciae/rps/rps_lje.py +++ b/deel/influenciae/rps/rps_lje.py @@ -7,27 +7,20 @@ but using a local jacobian expansion, as per https://proceedings.neurips.cc/paper/2021/file/c460dc0f18fc309ac07306a4a55d2fd6-Paper.pdf """ -import itertools - import tensorflow as tf -from tensorflow.keras.models import Sequential # pylint: disable=E0611 - -from ..common import InfluenceModel -from ..common import InverseHessianVectorProductFactory -from ..influence import FirstOrderInfluenceCalculator +from .base_representer_point import BaseRepresenterPoint +from ..common import InfluenceModel, InverseHessianVectorProductFactory from ..types import Union, Optional -class RepresenterPointLJE(FirstOrderInfluenceCalculator): +class RepresenterPointLJE(BaseRepresenterPoint): """ 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 +37,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,81 +47,112 @@ 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]) + 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) + 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 = dataset + else: + 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 + 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 perturbed head + self.perturbed_head = perturbed_head + + # Create the new model with the perturbed weights to compute the hessian matrix + 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 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) - - def _reshape_assign(self, weights, influence_vector: tf.Tensor) -> None: + def _compute_alpha(self, z_batch: tf.Tensor, y_batch: tf.Tensor) -> tf.Tensor: """ - Updates the model's weights in-place for the Local Jacobian Expansion approximation. + Computes the alpha vector 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. + 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. """ - 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) + # 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( # pylint: disable=protected-access + tf.expand_dims(v, axis=0), + use_gradient=False + ), + grads + ) + 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(list(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 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..5f98392 100644 --- a/deel/influenciae/utils/tf_operations.py +++ b/deel/influenciae/utils/tf_operations.py @@ -57,6 +57,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]: """ diff --git a/docs/api/Boundary-based/sample_boundary.md b/docs/api/Boundary-based/sample_boundary.md new file mode 100644 index 0000000..853eef0 --- /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://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 new file mode 100644 index 0000000..62a3b37 --- /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}_{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). + +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://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/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}} diff --git a/docs/index.md b/docs/index.md index 1497058..7bdcf7f 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 @@ -168,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. 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", 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) 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) 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(): """ 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), diff --git a/tests/rps/test_rps_lje.py b/tests/rps/test_rps_lje.py index f0da0e2..61268e9 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 = tf.abs(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 = tf.abs(alpha_i) - 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, percent=0.05) -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)) 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))) 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)