interpreto 0.4.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. interpreto/__init__.py +80 -0
  2. interpreto/attributions/__init__.py +50 -0
  3. interpreto/attributions/aggregations/__init__.py +36 -0
  4. interpreto/attributions/aggregations/base.py +159 -0
  5. interpreto/attributions/aggregations/linear_regression_aggregation.py +194 -0
  6. interpreto/attributions/aggregations/sobol_aggregation.py +116 -0
  7. interpreto/attributions/base.py +775 -0
  8. interpreto/attributions/methods/__init__.py +47 -0
  9. interpreto/attributions/methods/gradient_shap.py +119 -0
  10. interpreto/attributions/methods/integrated_gradients.py +114 -0
  11. interpreto/attributions/methods/kernel_shap.py +128 -0
  12. interpreto/attributions/methods/lime.py +137 -0
  13. interpreto/attributions/methods/occlusion.py +115 -0
  14. interpreto/attributions/methods/saliency.py +107 -0
  15. interpreto/attributions/methods/smooth_grad.py +118 -0
  16. interpreto/attributions/methods/sobol_attribution.py +134 -0
  17. interpreto/attributions/methods/square_grad.py +118 -0
  18. interpreto/attributions/methods/var_grad.py +122 -0
  19. interpreto/attributions/metrics/__init__.py +25 -0
  20. interpreto/attributions/metrics/insertion_deletion.py +279 -0
  21. interpreto/attributions/perturbations/__init__.py +33 -0
  22. interpreto/attributions/perturbations/base.py +312 -0
  23. interpreto/attributions/perturbations/gaussian_noise_perturbation.py +81 -0
  24. interpreto/attributions/perturbations/gradient_shap_perturbation.py +81 -0
  25. interpreto/attributions/perturbations/insertion_deletion_perturbation.py +163 -0
  26. interpreto/attributions/perturbations/linear_interpolation_perturbation.py +134 -0
  27. interpreto/attributions/perturbations/occlusion_perturbation.py +82 -0
  28. interpreto/attributions/perturbations/random_perturbation.py +91 -0
  29. interpreto/attributions/perturbations/shap_perturbation.py +122 -0
  30. interpreto/attributions/perturbations/sobol_perturbation.py +137 -0
  31. interpreto/attributions/plots/__init__.py +0 -0
  32. interpreto/commons/__init__.py +28 -0
  33. interpreto/commons/distances.py +271 -0
  34. interpreto/commons/generator_tools.py +168 -0
  35. interpreto/commons/granularity.py +653 -0
  36. interpreto/concepts/__init__.py +67 -0
  37. interpreto/concepts/base.py +495 -0
  38. interpreto/concepts/interpretations/__init__.py +29 -0
  39. interpreto/concepts/interpretations/base.py +554 -0
  40. interpreto/concepts/interpretations/llm_labels.py +508 -0
  41. interpreto/concepts/interpretations/topk_inputs.py +335 -0
  42. interpreto/concepts/methods/__init__.py +71 -0
  43. interpreto/concepts/methods/cockatiel.py +91 -0
  44. interpreto/concepts/methods/neurons_as_concepts.py +152 -0
  45. interpreto/concepts/methods/overcomplete.py +722 -0
  46. interpreto/concepts/methods/sklearn_wrappers.py +420 -0
  47. interpreto/concepts/metrics/__init__.py +45 -0
  48. interpreto/concepts/metrics/consim.py +1335 -0
  49. interpreto/concepts/metrics/dictionary_metrics.py +198 -0
  50. interpreto/concepts/metrics/reconstruction_metrics.py +161 -0
  51. interpreto/concepts/metrics/sparsity_metrics.py +96 -0
  52. interpreto/concepts/plots/__init__.py +0 -0
  53. interpreto/model_wrapping/__init__.py +27 -0
  54. interpreto/model_wrapping/classification_inference_wrapper.py +227 -0
  55. interpreto/model_wrapping/generation_inference_wrapper.py +214 -0
  56. interpreto/model_wrapping/inference_wrapper.py +693 -0
  57. interpreto/model_wrapping/llm_interface.py +149 -0
  58. interpreto/model_wrapping/model_with_split_points.py +1408 -0
  59. interpreto/model_wrapping/splitting_utils.py +125 -0
  60. interpreto/model_wrapping/transformers_classes.py +88 -0
  61. interpreto/typing.py +98 -0
  62. interpreto/visualizations/__init__.py +0 -0
  63. interpreto/visualizations/attributions/__init__.py +5 -0
  64. interpreto/visualizations/attributions/attribution_highlight.py +323 -0
  65. interpreto/visualizations/base.py +237 -0
  66. interpreto/visualizations/colormap_helpers.py +59 -0
  67. interpreto/visualizations/concepts/__init__.py +5 -0
  68. interpreto/visualizations/concepts/concepts_highlight.py +176 -0
  69. interpreto/visualizations/visualization.css +129 -0
  70. interpreto/visualizations/visualization.js +606 -0
  71. interpreto/visualizations/visualization_attribution.js +591 -0
  72. interpreto-0.4.9.dist-info/METADATA +279 -0
  73. interpreto-0.4.9.dist-info/RECORD +76 -0
  74. interpreto-0.4.9.dist-info/WHEEL +5 -0
  75. interpreto-0.4.9.dist-info/licenses/LICENSE +23 -0
  76. interpreto-0.4.9.dist-info/top_level.txt +1 -0
@@ -0,0 +1,775 @@
1
+ # MIT License
2
+ #
3
+ # Copyright (c) 2025 IRT Antoine de Saint Exupéry et Université Paul Sabatier Toulouse III - All
4
+ # rights reserved. DEEL and FOR are research programs operated by IVADO, IRT Saint Exupéry,
5
+ # CRIAQ and ANITI - https://www.deel.ai/.
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"), to deal
9
+ # in the Software without restriction, including without limitation the rights
10
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ # copies of the Software, and to permit persons to whom the Software is
12
+ # furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included in all
15
+ # copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ # SOFTWARE.
24
+
25
+ """
26
+ Basic standard classes for attribution methods
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import itertools
32
+ from abc import abstractmethod
33
+ from collections.abc import Callable, Iterable, MutableMapping
34
+ from enum import Enum
35
+ from typing import Any
36
+
37
+ import torch
38
+ from beartype import beartype
39
+ from jaxtyping import Float, Int, jaxtyped
40
+ from transformers import BatchEncoding, PreTrainedModel, PreTrainedTokenizer
41
+
42
+ from interpreto.attributions.aggregations.base import Aggregator
43
+ from interpreto.attributions.perturbations.base import Perturbator
44
+ from interpreto.commons import Granularity
45
+ from interpreto.commons.generator_tools import split_iterator
46
+ from interpreto.commons.granularity import GranularityAggregationStrategy
47
+ from interpreto.model_wrapping.classification_inference_wrapper import ClassificationInferenceWrapper
48
+ from interpreto.model_wrapping.generation_inference_wrapper import GenerationInferenceWrapper
49
+ from interpreto.model_wrapping.inference_wrapper import InferenceModes, InferenceWrapper
50
+ from interpreto.typing import ClassificationTarget, GeneratedTarget, ModelInputs, SingleAttribution, TensorMapping
51
+
52
+
53
+ class ModelTask(Enum):
54
+ """
55
+ Enum to represent the model task type.
56
+ """
57
+
58
+ SINGLE_CLASS_CLASSIFICATION = "single-class classification"
59
+ MULTI_CLASS_CLASSIFICATION = "multi-class classification"
60
+ GENERATION = "generation"
61
+
62
+
63
+ def clone_tensor_mapping(tm: TensorMapping, detach: bool = False) -> TensorMapping:
64
+ """
65
+ Clone a TensorMapping, optionally detaching the tensors.
66
+
67
+ Args:
68
+ tm (TensorMapping): tensor mapping to clone
69
+ detach (bool, optional): specify if new tensors must be detached. Defaults to False.
70
+
71
+ Returns:
72
+ TensorMapping: cloned tensor mapping
73
+ """
74
+ return {k: v.detach().clone() if detach else v.clone() for k, v in tm.items()}
75
+
76
+
77
+ class AttributionOutput:
78
+ """
79
+ Class to store the output of an attribution method.
80
+ """
81
+
82
+ __slots__ = (
83
+ "attributions",
84
+ "elements",
85
+ "model_inputs_to_explain",
86
+ "targets",
87
+ "model_task",
88
+ "classes",
89
+ "granularity",
90
+ "inference_mode",
91
+ )
92
+
93
+ def __init__(
94
+ self,
95
+ attributions: SingleAttribution,
96
+ elements: list[str] | torch.Tensor,
97
+ model_inputs_to_explain: TensorMapping,
98
+ targets: torch.Tensor,
99
+ model_task: ModelTask,
100
+ classes: torch.Tensor | None = None,
101
+ granularity: Granularity = Granularity.DEFAULT,
102
+ inference_mode: Callable[[torch.Tensor], torch.Tensor] = InferenceModes.LOGITS,
103
+ ):
104
+ """
105
+ Initializes an AttributionOutput instance.
106
+
107
+ # TODO: Harmonize even more, all attributions could be of the shape (t, l),
108
+ # with t being either a number of class or of generated tokens.
109
+ # It should not be a problem if some values are None or zero for generation.
110
+ # This should be thoroughly tested.
111
+
112
+ Args:
113
+ attributions (Iterable[SingleAttribution]): A list (n elements, with n the number of samples) of attribution score tensors:
114
+ - `l` represents the number of elements for which attribution is computed (for NLP tasks: can be the total sequence length).
115
+ - Shapes depend on the task:
116
+ - Classification (single class): `(l)`
117
+ - Classification (multi classes): `(c, l)`, where `c` is the number of classes.
118
+ - Generative models: `(l_g, l)`, where `l_g` is the length of the generated part.
119
+ - For non-generated elements, there are `l_g` attribution scores.
120
+ - For generated elements, scores are zero for previously generated tokens.
121
+ - Token classification: `(l_t, l)`, where `l_t` is the number of token classes. When the tokens are disturbed, l = l_t.
122
+ elements (Iterable[list[str]] | Iterable[torch.Tensor]): A list or tensor representing the elements for which attributions are computed.
123
+ - These elements can be tokens, words, sentences, or tensors of size `l`.
124
+ model_task (ModelTask): An enum representing the task of the model explained, such as SINGLE_CLASS_CLASSIFICATION, MULTI_CLASS_CLASSIFICATION, or GENERATION.
125
+ classes (torch.Tensor | None): Optional tensor of class labels.
126
+ - For single-class classification: tensor of shape `(1)`
127
+ - For multi-class classification: tensor of shape `(c)` where `c` is the number of classes
128
+ """
129
+ self.attributions = attributions
130
+ self.elements = elements
131
+ self.model_inputs_to_explain = model_inputs_to_explain
132
+ self.targets = targets
133
+ self.model_task = model_task
134
+ self.classes = classes
135
+ self.granularity = granularity
136
+ self.inference_mode = inference_mode
137
+
138
+ def __repr__(self):
139
+ return (
140
+ f"AttributionOutput("
141
+ f"attributions={repr(self.attributions)}, "
142
+ f"elements={repr(self.elements)}, "
143
+ f"model_inputs_to_explain={repr(self.model_inputs_to_explain)}, "
144
+ f"targets={repr(self.targets)}, "
145
+ f"model_task='{self.model_task}', "
146
+ f"classes={repr(self.classes)}), "
147
+ f"granularity={self.granularity}, "
148
+ f"inference_mode={self.inference_mode.__name__}"
149
+ )
150
+
151
+ def __str__(self):
152
+ return (
153
+ f"AttributionOutput("
154
+ f"attributions={self.attributions}, "
155
+ f"elements={self.elements}, "
156
+ f"model_inputs_to_explain={self.model_inputs_to_explain}, "
157
+ f"targets={self.targets}, "
158
+ f"model_task='{self.model_task}', "
159
+ f"classes={self.classes}), "
160
+ f"granularity={self.granularity}, "
161
+ f"inference_mode={self.inference_mode.__name__}"
162
+ )
163
+
164
+
165
+ class AttributionExplainer:
166
+ """
167
+ Abstract base class for attribution explainers.
168
+
169
+ This class defines a common interface and helper methods used by various attribution explainers.
170
+ Subclasses must implement the abstract method 'explain'.
171
+ """
172
+
173
+ _associated_inference_wrapper = InferenceWrapper
174
+
175
+ def __init__(
176
+ self,
177
+ model: PreTrainedModel,
178
+ tokenizer: PreTrainedTokenizer,
179
+ batch_size: int = 4,
180
+ perturbator: Perturbator | None = None,
181
+ aggregator: Aggregator | None = None,
182
+ device: torch.device | None = None,
183
+ granularity: Granularity = Granularity.DEFAULT,
184
+ granularity_aggregation_strategy: GranularityAggregationStrategy = GranularityAggregationStrategy.MEAN,
185
+ inference_mode: Callable[[torch.Tensor], torch.Tensor] = InferenceModes.LOGITS, # TODO: add to all classes
186
+ use_gradient: bool = False,
187
+ input_x_gradient: bool = True,
188
+ ) -> None:
189
+ """
190
+ Initializes the AttributionExplainer.
191
+
192
+ Args:
193
+ model (PreTrainedModel): The model to be explained.
194
+ tokenizer (PreTrainedTokenizer): The tokenizer associated with the model.
195
+ batch_size (int): The batch size used for model inference.
196
+ perturbator (Perturbator, optional): Instance used to generate input perturbations.
197
+ If None, the perturbator returns only the original input.
198
+ aggregator (Aggregator, optional): Instance used to aggregate computed attribution scores.
199
+ If None, the aggregator returns the original scores.
200
+ device (torch.device, optional): The device on which computations are performed.
201
+ If None, defaults to the device of the model.
202
+ granularity (Granularity, optional): The level of granularity for the explanation.
203
+ Options are: `ALL_TOKENS`, `TOKEN`, `WORD`, or `SENTENCE`.
204
+ Defaults to Granularity.DEFAULT (ALL_TOKENS)
205
+ To obtain it, `from interpreto import Granularity` then `Granularity.WORD`.
206
+ granularity_aggregation_strategy (GranularityAggregationStrategy, optional): The method used to aggregate scores at the specified granularity,
207
+ for gradient-based methods. Thus, it is ignored for perturbation based methods.
208
+ Defaults to GranularityAggregationStrategy.MEAN.
209
+ Ignored for `granularity` set to `ALL_TOKENS` or `TOKEN`.
210
+ inference_mode (Callable[[torch.Tensor], torch.Tensor], optional): The mode used for inference.
211
+ It can be either one of LOGITS, SOFTMAX, or LOG_SOFTMAX. Use InferenceModes to choose the appropriate mode.
212
+ use_gradient (bool, optional): If True, computes gradients instead of inference for targeted explanations.
213
+ input_x_gradient (bool, optional): If True and ``use_gradient`` is set, multiplies the input embeddings
214
+ with their gradients before reducing them. Defaults to ``True``.
215
+ """
216
+ self.use_gradient = use_gradient
217
+ self.input_x_gradient = input_x_gradient
218
+ if not hasattr(self, "tokenizer"):
219
+ model, _ = self._set_tokenizer(model, tokenizer)
220
+ self.inference_wrapper = self._associated_inference_wrapper(
221
+ model, batch_size=batch_size, device=device, mode=inference_mode
222
+ ) # type: ignore
223
+ self.perturbator = perturbator or Perturbator()
224
+ self.perturbator.to(self.device)
225
+ self.aggregator = aggregator or Aggregator()
226
+ self.granularity = granularity
227
+ self.granularity_aggregation_strategy = granularity_aggregation_strategy
228
+ self.inference_wrapper.pad_token_id = self.tokenizer.pad_token_id
229
+
230
+ def _set_tokenizer(self, model, tokenizer) -> tuple[PreTrainedModel, int]:
231
+ self.tokenizer = tokenizer
232
+ # replace token for perturbations
233
+ replace_token = "[REPLACE]"
234
+ if replace_token not in tokenizer.get_vocab():
235
+ tokenizer.add_tokens([replace_token])
236
+
237
+ # add a pad token if it does not exist
238
+ if self.tokenizer.pad_token is None:
239
+ self.tokenizer.add_special_tokens({"pad_token": "<pad>"})
240
+
241
+ # resize model with new tokens
242
+ model.resize_token_embeddings(len(self.tokenizer))
243
+
244
+ replace_token_id = self.tokenizer.convert_tokens_to_ids(replace_token)
245
+ if isinstance(replace_token_id, list):
246
+ replace_token_id = replace_token_id[0]
247
+
248
+ return model, replace_token_id
249
+
250
+ def get_scores(
251
+ self,
252
+ model_inputs: Iterable[TensorMapping],
253
+ targets: Iterable[torch.Tensor],
254
+ ) -> Iterable[torch.Tensor]:
255
+ """
256
+ Computes scores for the given perturbations and targets.
257
+
258
+ Args:
259
+ pert_generator (Iterable[TensorMapping]): An iterable of perturbed model inputs.
260
+ targets (torch.Tensor): The target classes or tokens.
261
+
262
+ Returns:
263
+ Iterable[torch.Tensor]: The computed scores.
264
+ """
265
+ if self.use_gradient:
266
+ return self.inference_wrapper.get_gradients(model_inputs, targets, input_x_gradient=self.input_x_gradient)
267
+ return self.inference_wrapper.get_targeted_logits(model_inputs, targets)
268
+
269
+ @property
270
+ def device(self) -> torch.device:
271
+ """
272
+ Returns the device on which the model is located.
273
+ """
274
+ return self.inference_wrapper.device
275
+
276
+ @device.setter
277
+ def device(self, device: torch.device) -> None:
278
+ """
279
+ Sets the device on which the model is located.
280
+ """
281
+ self.inference_wrapper.device = device
282
+
283
+ def to(self, device: torch.device) -> None:
284
+ """
285
+ Moves the model to the specified device.
286
+
287
+ Args:
288
+ device (torch.device): The device to which the model should be moved.
289
+ """
290
+ self.inference_wrapper.to(device)
291
+
292
+ def process_model_inputs(self, model_inputs: ModelInputs) -> list[TensorMapping]:
293
+ """
294
+ Processes and standardizes model inputs into a list of dictionaries compatible with the model.
295
+
296
+ This method handles various input types:
297
+ - If a string is provided, it tokenizes the string and returns a list containing one mapping.
298
+ - If a mapping is provided with a batch (multiple samples), it splits the batch into individual mappings.
299
+ - If an iterable is provided, it processes each item recursively.
300
+
301
+ Args:
302
+ model_inputs (str, TensorMapping, or Iterable): The raw model inputs.
303
+
304
+ Returns:
305
+ List[TensorMapping]: A list of processed model input mappings.
306
+
307
+ Raises:
308
+ ValueError: If the type of model_inputs is not supported.
309
+ """
310
+ if isinstance(model_inputs, str):
311
+ return [self.tokenizer(model_inputs, return_tensors="pt", return_offsets_mapping=True, truncation=True)]
312
+ if isinstance(
313
+ model_inputs, BatchEncoding
314
+ ): # we cant use TensorMapping in the isinstance so we use MutableMapping.
315
+ splitted_encodings = []
316
+ for i, enc in enumerate(model_inputs.encodings): # type: ignore # one Encoding per row
317
+ data_i = {
318
+ k: (v[i].unsqueeze(0) if isinstance(v, torch.Tensor) else [v[i]]) for k, v in model_inputs.items()
319
+ }
320
+ splitted_encodings.append(
321
+ BatchEncoding(
322
+ data=data_i, # tensors/arrays for that row
323
+ encoding=enc, # its Encoding (keeps word_ids, offsets…) necessary for granularity
324
+ tensor_type="pt", # keep tensors if you had them
325
+ )
326
+ )
327
+ return splitted_encodings
328
+ if isinstance(model_inputs, Iterable):
329
+ return list(itertools.chain(*[self.process_model_inputs(item) for item in model_inputs]))
330
+ raise ValueError(
331
+ f"type {type(model_inputs)} not supported for method process_model_inputs in class {self.__class__.__name__}"
332
+ )
333
+
334
+ @abstractmethod
335
+ def process_inputs_to_explain_and_targets(
336
+ self,
337
+ model_inputs: Iterable[TensorMapping],
338
+ targets: torch.Tensor | Iterable[torch.Tensor] | None = None,
339
+ **model_kwargs: Any,
340
+ ) -> tuple[Iterable[TensorMapping], Iterable[Float[torch.Tensor, "n t"]]]:
341
+ """
342
+ Processes the inputs and targets for explanation.
343
+
344
+ This method must be implemented by subclasses.
345
+
346
+ Args:
347
+ model_inputs (Iterable[TensorMapping]): The inputs to the model.
348
+ targets (Any): The targets to be explained.
349
+ model_kwargs (Any): Additional model-specific arguments.
350
+
351
+ Returns:
352
+ tuple: A tuple of (processed_inputs, processed_targets).
353
+
354
+ Raises:
355
+ NotImplementedError: Always raised. Subclasses must implement this method.
356
+ """
357
+ raise NotImplementedError(
358
+ "Specific task subclasses must implement the 'process_inputs_to_explain_and_targets' method "
359
+ "to correctly process inputs and targets for explanations."
360
+ )
361
+
362
+ def explain(
363
+ self,
364
+ model_inputs: ModelInputs,
365
+ targets: (
366
+ torch.Tensor | Iterable[torch.Tensor] | None
367
+ ) = None, # TODO: create specific target type for classification and generation
368
+ **model_kwargs: Any,
369
+ ) -> Iterable[AttributionOutput]:
370
+ """
371
+ Computes attributions for NLP models.
372
+
373
+ Process:
374
+ 1. Process and standardize the model inputs.
375
+ 2. Create the tokenizer's pad token if not already set and add it to the inference wrapper.
376
+ 3. If targets are not provided, create them. Otherwise, for each input-target pair, process them.
377
+ 4. Decompose the inputs based on the desired granularity and decode tokens.
378
+ 5. Generate perturbations for the constructed inputs.
379
+ 6. Compute scores using either gradients (if use_gradient is True) or targeted logits.
380
+ 7. Aggregate the scores to obtain contribution values.
381
+
382
+ Args:
383
+ model_inputs (ModelInputs): Raw inputs for the model.
384
+ targets (torch.Tensor | Iterable[torch.Tensor] | None): Targets for which explanations are desired.
385
+ Further types might be supported by sub-classes.
386
+ It depends on the task:
387
+ - For classification tasks, encodes the target class or classes to explain.
388
+ - For generation tasks, encodes the target text or tokens to explain.
389
+
390
+ Returns:
391
+ List[AttributionOutput]: A list of attribution outputs, one per input sample.
392
+ """
393
+ # Ensure the model inputs are in the correct format
394
+ sanitized_model_inputs: Iterable[TensorMapping] = self.process_model_inputs(model_inputs)
395
+
396
+ # Process the inputs and targets for explanation
397
+ # If targets are not provided, create them from model_inputs_to_explain.
398
+ model_inputs_to_explain: Iterable[TensorMapping]
399
+ sanitized_targets: Iterable[Float[torch.Tensor, "t"]]
400
+ model_inputs_to_explain, sanitized_targets_gen = self.process_inputs_to_explain_and_targets(
401
+ sanitized_model_inputs, targets, **model_kwargs
402
+ )
403
+ sanitized_targets = list(sanitized_targets_gen)
404
+
405
+ # Create perturbation masks and perturb inputs based on the masks.
406
+ # Inputs might be embedded during the perturbation process if the perturbator works with embeddings.
407
+ pert_generator: Iterable[TensorMapping]
408
+ mask_generator: Iterable[torch.Tensor | None]
409
+ pert_generator, mask_generator = split_iterator(self.perturbator.perturb(m) for m in model_inputs_to_explain)
410
+
411
+ # Compute the score on perturbed inputs:
412
+ # - If use_gradient is True, compute gradients.
413
+ # - Otherwise, compute targeted logits.
414
+ scores: Iterable[torch.Tensor] = self.get_scores(
415
+ pert_generator, (a.to(self.device) for a in sanitized_targets)
416
+ )
417
+
418
+ # Aggregate the scores using the aggregator function and the perturbation masks.
419
+ # Aggregation over perturbations: (p, t), (p, l) -> (t, l)
420
+ contributions = (
421
+ self.aggregator(score.detach(), mask.to(self.device) if mask is not None else None)
422
+ for score, mask in zip(scores, mask_generator, strict=True)
423
+ )
424
+
425
+ # Aggregate the score with respect to the granularity level
426
+ # - Aggregate over the inputs for gradient-based methods: (t, l) -> (t, lg)
427
+ # - Aggregate over the targets if the model is a generation model: (t, l) -> (tg, l)
428
+ granular_contributions = (
429
+ Granularity.granularity_score_aggregation(
430
+ contribution=contribution.cpu(),
431
+ granularity=self.granularity,
432
+ granularity_aggregation_strategy=self.granularity_aggregation_strategy,
433
+ inputs=inputs, # type: ignore
434
+ tokenizer=self.tokenizer,
435
+ aggregate_inputs=self.use_gradient, # Gradient-based methods
436
+ aggregate_targets=isinstance(self.inference_wrapper, GenerationInferenceWrapper), # Generation models
437
+ )
438
+ for contribution, inputs in zip(contributions, model_inputs_to_explain, strict=True)
439
+ )
440
+
441
+ # Decompose each input for the desired granularity level (tokens, words, sentences...)
442
+ granular_inputs_texts: list[list[str]] = [
443
+ Granularity.get_decomposition(inputs, self.granularity, self.tokenizer, return_text=True)[0] # type: ignore
444
+ for inputs in model_inputs_to_explain
445
+ ]
446
+
447
+ # Create and return AttributionOutput objects with the contributions and decoded token sequences:
448
+ results = []
449
+ for contribution, model_input, elements, target in zip(
450
+ granular_contributions, model_inputs_to_explain, granular_inputs_texts, sanitized_targets, strict=True
451
+ ):
452
+ if self.inference_wrapper.__class__.__name__ == "GenerationInferenceWrapper":
453
+ model_task = ModelTask.GENERATION
454
+ t, l = contribution.shape
455
+ mask = torch.triu(torch.ones((t, l), dtype=torch.bool), diagonal=l - t)
456
+ contribution[mask] = float("nan")
457
+ classes = None
458
+ elif self.inference_wrapper.__class__.__name__ == "ClassificationInferenceWrapper":
459
+ classes = target
460
+ if contribution.shape[0] == 1:
461
+ model_task = ModelTask.SINGLE_CLASS_CLASSIFICATION
462
+ else:
463
+ model_task = ModelTask.MULTI_CLASS_CLASSIFICATION
464
+ else:
465
+ raise NotImplementedError(
466
+ f"Model type {self.inference_wrapper.model.__class__.__name__} not supported for AttributionExplainer."
467
+ )
468
+
469
+ # sanitize model_input
470
+ _ = model_input.pop("inputs_embeds", None)
471
+ model_input["attention_mask"] = model_input["attention_mask"][0].unsqueeze(dim=0)
472
+
473
+ # construct attribution output
474
+ attribution_output = AttributionOutput(
475
+ attributions=contribution,
476
+ elements=elements,
477
+ model_inputs_to_explain=model_input,
478
+ model_task=model_task,
479
+ classes=classes,
480
+ targets=target.cpu(), # TODO: manage target device in the inference wrapper
481
+ granularity=self.granularity,
482
+ inference_mode=self.inference_wrapper.mode,
483
+ )
484
+ results.append(attribution_output)
485
+ return results
486
+
487
+ def __call__(self, model_inputs: ModelInputs, targets=None, **kwargs) -> Iterable[AttributionOutput]:
488
+ """
489
+ Enables the explainer instance to be called as a function.
490
+
491
+ Args:
492
+ model_inputs (ModelInputs): Raw inputs for the model.
493
+ targets: Targets for which explanations are desired. It depends on the task:
494
+ - For classification tasks, encodes the target class or classes to explain.
495
+ - For generation tasks, encodes the target text or tokens to explain.
496
+
497
+ Returns:
498
+ List[AttributionOutput]: A list of attribution outputs, one per input sample.
499
+ """
500
+ return self.explain(model_inputs, targets, **kwargs)
501
+
502
+
503
+ class ClassificationAttributionExplainer(AttributionExplainer):
504
+ """
505
+ Attribution explainer for classification models
506
+ """
507
+
508
+ _associated_inference_wrapper = ClassificationInferenceWrapper
509
+ inference_wrapper: ClassificationInferenceWrapper
510
+
511
+ def process_targets(
512
+ self, targets: ClassificationTarget, expected_length: int | None = None
513
+ ) -> Iterable[Int[torch.Tensor, "t"]]:
514
+ """
515
+ Normalize classification targets into a list of 1D integer tensors.
516
+
517
+ Parameters
518
+ ----------
519
+ targets : int | Int[torch.Tensor, "n"] | Int[torch.Tensor, "n t"] | Iterable[int] | Iterable[Int[torch.Tensor, "t"]]
520
+ The classification target(s). Supported formats include:
521
+ - A single integer: Interpreted as a single target.
522
+ - A 1D or 2D integer torch.Tensor:
523
+ * 1D tensors are treated as a sequence of individual targets.
524
+ * 2D tensors must have shape (n, t), where `n` is the number of targets.
525
+ - An iterable of integers: Each integer is treated as a separate target.
526
+ - An iterable of 1D integer torch.Tensors: Each tensor must be 1D and contain integers.
527
+
528
+ expected_length : int | None, optional
529
+ If specified, validates that the number of targets matches this expected length.
530
+
531
+ Returns
532
+ -------
533
+ Iterable[Int[torch.Tensor, "t"]]
534
+ A list of 1D integer tensors, one per input instance.
535
+
536
+ Raises
537
+ ------
538
+ ValueError
539
+ - If the number of targets does not match `expected_length`.
540
+ TypeError
541
+ - If the type of `targets` is unsupported.
542
+ - If tensor targets are not 1D or 2D.
543
+ - If tensor values are not integers.
544
+ """
545
+ # integer
546
+ if isinstance(targets, int):
547
+ if expected_length is not None and expected_length != 1:
548
+ raise ValueError(
549
+ "Mismatch between the inputs and targets length."
550
+ + f" Target is a single integer, but the length of the inputs is {expected_length}."
551
+ )
552
+ return [torch.tensor([targets])]
553
+
554
+ # tensor
555
+ if isinstance(targets, torch.Tensor):
556
+ if targets.ndim == 1:
557
+ # one dimensional tensors are treated as iterable of integer targets
558
+ targets = targets.unsqueeze(-1)
559
+ if expected_length is not None and expected_length != targets.shape[0]:
560
+ raise ValueError(
561
+ "Mismatch between the inputs and targets length."
562
+ + f" Target tensor of {targets.shape[0]} elements, but the length of the inputs is {expected_length}."
563
+ )
564
+ if targets.ndim != 2: # actually verified by jaxtyping
565
+ raise TypeError(
566
+ "Target tensor must be one-dimensional or two-dimensional."
567
+ + f" Target tensor has {targets.ndim} dimensions."
568
+ )
569
+ if torch.is_floating_point(targets): # actually verified by jaxtyping
570
+ raise TypeError("Target tensor must be integers.")
571
+ return targets.unbind(dim=0)
572
+
573
+ # iterable
574
+ if isinstance(targets, Iterable):
575
+ if expected_length is not None and len(targets) != expected_length: # type: ignore
576
+ raise ValueError(
577
+ "Mismatch between the inputs and targets length."
578
+ + f" Target is an iterable of {len(targets)} elements, but the length of the inputs is {expected_length}." # type: ignore
579
+ )
580
+
581
+ # iterable[int]
582
+ if all(isinstance(t, int) for t in targets): # actually verified by jaxtyping
583
+ return [torch.tensor([target]) for target in targets]
584
+
585
+ # iterable[torch.Tensor]
586
+ iterable_targets: Iterable[torch.Tensor] = targets # type: ignore
587
+ if all(isinstance(t, torch.Tensor) for t in iterable_targets): # actually verified by jaxtyping
588
+ if any(target.ndim != 1 for target in iterable_targets):
589
+ raise TypeError("If the targets are iterable of tensors, the tensors must be one-dimensional.")
590
+ if any(torch.is_floating_point(target) for target in iterable_targets):
591
+ raise TypeError("If the targets are iterable of tensors, they must be integers.")
592
+ return iterable_targets
593
+
594
+ raise TypeError(f"Target type {type(targets)} not supported.")
595
+
596
+ @jaxtyped(typechecker=beartype)
597
+ def process_inputs_to_explain_and_targets(
598
+ self,
599
+ model_inputs: Iterable[TensorMapping],
600
+ targets: ClassificationTarget | None = None,
601
+ **model_kwargs: Any,
602
+ ) -> tuple[Iterable[TensorMapping], Iterable[torch.Tensor]]:
603
+ """
604
+ Pre-processes model inputs and classification targets for explanation.
605
+
606
+ This method ensures that:
607
+ - If `targets` are not provided, they are computed by performing inference on `model_inputs` and selecting the predicted class using `argmax`.
608
+ - The `targets` are then validated and converted using `self.process_targets`, ensuring the same length as `model_inputs`.
609
+
610
+ Parameters
611
+ ----------
612
+ model_inputs : Iterable[TensorMapping]
613
+ A batch of input mappings, typically containing tokenized inputs such as "input_ids", "attention_mask", etc.
614
+
615
+ targets : int | torch.Tensor | Iterable[int] | Iterable[torch.Tensor] | None, optional
616
+ Classification targets for each input. If None, targets are computed using model inference
617
+ by selecting the index with the highest logit value for each input.
618
+
619
+ **model_kwargs : Any
620
+ Additional keyword arguments passed to the model during inference, if targets are inferred.
621
+
622
+ Returns
623
+ -------
624
+ tuple[Iterable[TensorMapping], Iterable[torch.Tensor]]
625
+ - model_inputs_to_explain: List of tokenized input mappings with required explanation metadata (e.g., special tokens mask).
626
+ - sanitized_targets: List of 1D integer tensors, each corresponding to a target label for an input.
627
+
628
+ Raises
629
+ ------
630
+ ValueError
631
+ If the provided or inferred targets do not match the number of input instances, or if their format is invalid.
632
+ """
633
+ if targets is None:
634
+ # compute targets from logits if not provided
635
+ sanitized_targets: Iterable[torch.Tensor] = self.inference_wrapper.get_targets(model_inputs) # type: ignore
636
+ else:
637
+ # process targets and ensure they have the same length as inputs
638
+ expected_targets_length = len(model_inputs) # type: ignore
639
+ sanitized_targets: Iterable[torch.Tensor] = self.process_targets(targets, expected_targets_length) # type: ignore
640
+
641
+ return model_inputs, sanitized_targets
642
+
643
+
644
+ class GenerationAttributionExplainer(AttributionExplainer):
645
+ """
646
+ Attribution explainer for generation models
647
+ """
648
+
649
+ _associated_inference_wrapper = GenerationInferenceWrapper
650
+ inference_wrapper: GenerationInferenceWrapper
651
+
652
+ @jaxtyped(typechecker=beartype)
653
+ def process_targets(self, targets: GeneratedTarget, expected_length: int | None = None) -> list[torch.Tensor]:
654
+ """
655
+ Processes the target inputs for generative models into a standardized format.
656
+
657
+ This function handles various input types for targets (string, TensorMapping, or Iterable)
658
+ and converts them into a list of tensors containing token IDs.
659
+
660
+ Args:
661
+ targets (str, TensorMapping, torch.Tensor, or Iterable): The target texts or tokens.
662
+
663
+ Returns:
664
+ List[torch.Tensor]: A list of 1-D tensors representing the target token IDs.
665
+
666
+ Raises:
667
+ ValueError: If the target type is not supported.
668
+ """
669
+ if isinstance(targets, str):
670
+ targets = self.tokenizer(targets, return_tensors="pt", truncation=True)["input_ids"].squeeze(dim=0)
671
+ return [targets] # type: ignore
672
+ if isinstance(targets, MutableMapping): # TensorMapping cannot be used in isinstance
673
+ targets = targets["input_ids"]
674
+ if targets.dim() == 1:
675
+ return list(targets)
676
+ if targets.shape[0] > 1:
677
+ targets = targets.split(1, dim=0) # If the batch size > 1, we cut into a list of n mappings.
678
+ return [t.squeeze(dim=0) for t in targets] # type: ignore
679
+ return [targets.squeeze(dim=0)]
680
+ if isinstance(targets, torch.Tensor):
681
+ targets = targets.squeeze(dim=0) # remove batch dimension if any
682
+ assert targets.dim() == 1, "Target tensor must be 1-D."
683
+ return [targets]
684
+ if isinstance(targets, Iterable):
685
+ return list(itertools.chain(*[self.process_targets(item) for item in targets]))
686
+ raise ValueError(
687
+ f"type {type(targets)} not supported for method process_targets in class {self.__class__.__name__}"
688
+ )
689
+
690
+ @jaxtyped(typechecker=beartype)
691
+ def process_inputs_to_explain_and_targets(
692
+ self,
693
+ model_inputs: Iterable[TensorMapping],
694
+ targets: GeneratedTarget | None = None,
695
+ **model_kwargs,
696
+ ) -> tuple[Iterable[TensorMapping], Iterable[torch.Tensor]]:
697
+ """
698
+ Processes the inputs and targets for the generative model.
699
+ If targets are not provided, create them with model_inputs_to_explain. Otherwise, for each input-target pair:
700
+ a. Embed the input.
701
+ b. Embed the target and concatenate with the input embeddings.
702
+ c. Construct a new input mapping that includes both embeddings.
703
+ Then, add offsets mapping and special tokens mask.
704
+
705
+ Args:
706
+ model_inputs (ModelInputs): The raw inputs for the generative model.
707
+ targets (GeneratedTarget): The target texts or tokens for which explanations are desired.
708
+ model_kwargs (dict): Additional arguments for the generation process.
709
+
710
+ Returns:
711
+ tuple: A tuple containing a list of processed model inputs and a list of processed targets.
712
+ """
713
+ # TODO: verify that inputs and targets have the same length
714
+ sanitized_targets: list[torch.Tensor]
715
+ if targets is None:
716
+ model_inputs_to_explain, sanitized_targets = self.inference_wrapper.get_inputs_to_explain_and_targets(
717
+ model_inputs, **model_kwargs
718
+ )
719
+ # Remove batch dimension to align with targets in ClassificationExplainer (1-D tensor of shape (t,))
720
+ sanitized_targets = [t.squeeze(dim=0) if t.dim() >= 1 else t for t in sanitized_targets]
721
+ else:
722
+ sanitized_targets = self.process_targets(targets)
723
+ model_inputs_to_explain = []
724
+ for model_input, target in zip(model_inputs, sanitized_targets, strict=True):
725
+ target_2d = target.unsqueeze(dim=0) # add batch dimension for concatenation with model_input
726
+ model_inputs_to_explain.append(
727
+ {
728
+ "input_ids": torch.cat([model_input["input_ids"], target_2d], dim=1), # type: ignore
729
+ "attention_mask": torch.cat(
730
+ [model_input["attention_mask"], torch.ones_like(target_2d)], dim=1
731
+ ), # type: ignore
732
+ }
733
+ )
734
+
735
+ # Convert to a `BatchEncoding` object and add offsets mapping:
736
+ # TODO: see if it can be optimized, conversion might be necessary only for WORD and SENTENCE granularity
737
+ model_inputs_to_explain_text = [
738
+ self.tokenizer.decode(elem["input_ids"][0], skip_special_tokens=True) for elem in model_inputs_to_explain
739
+ ]
740
+ model_inputs_to_explain = [
741
+ self.tokenizer(
742
+ [model_inputs_to_explain_text],
743
+ return_tensors="pt",
744
+ return_offsets_mapping=True,
745
+ truncation=True,
746
+ )
747
+ for model_inputs_to_explain_text in model_inputs_to_explain_text
748
+ ]
749
+
750
+ return model_inputs_to_explain, sanitized_targets
751
+
752
+
753
+ class FactoryGeneratedMeta(type):
754
+ """
755
+ Metaclass to distinguish classes generated by the MultitaskExplainerMixin.
756
+ """
757
+
758
+
759
+ class MultitaskExplainerMixin(AttributionExplainer):
760
+ """
761
+ Mixin class to generate the appropriate Explainer based on the model type.
762
+ """
763
+
764
+ def __new__(cls, model: PreTrainedModel, *args: Any, **kwargs: Any) -> AttributionExplainer:
765
+ if isinstance(cls, FactoryGeneratedMeta):
766
+ return super().__new__(cls) # type: ignore
767
+ if model.__class__.__name__.endswith("ForSequenceClassification"):
768
+ t = FactoryGeneratedMeta("Classification" + cls.__name__, (cls, ClassificationAttributionExplainer), {})
769
+ return t.__new__(t, model, *args, **kwargs) # type: ignore
770
+ if model.__class__.__name__.endswith("ForCausalLM") or model.__class__.__name__.endswith("LMHeadModel"):
771
+ t = FactoryGeneratedMeta("Generation" + cls.__name__, (cls, GenerationAttributionExplainer), {})
772
+ return t.__new__(t, model, *args, **kwargs) # type: ignore
773
+ raise NotImplementedError(
774
+ "Model type not supported for Explainer. Use a ModelForSequenceClassification, a ModelForCausalLM model or a LMHeadModel model."
775
+ )