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.
- interpreto/__init__.py +80 -0
- interpreto/attributions/__init__.py +50 -0
- interpreto/attributions/aggregations/__init__.py +36 -0
- interpreto/attributions/aggregations/base.py +159 -0
- interpreto/attributions/aggregations/linear_regression_aggregation.py +194 -0
- interpreto/attributions/aggregations/sobol_aggregation.py +116 -0
- interpreto/attributions/base.py +775 -0
- interpreto/attributions/methods/__init__.py +47 -0
- interpreto/attributions/methods/gradient_shap.py +119 -0
- interpreto/attributions/methods/integrated_gradients.py +114 -0
- interpreto/attributions/methods/kernel_shap.py +128 -0
- interpreto/attributions/methods/lime.py +137 -0
- interpreto/attributions/methods/occlusion.py +115 -0
- interpreto/attributions/methods/saliency.py +107 -0
- interpreto/attributions/methods/smooth_grad.py +118 -0
- interpreto/attributions/methods/sobol_attribution.py +134 -0
- interpreto/attributions/methods/square_grad.py +118 -0
- interpreto/attributions/methods/var_grad.py +122 -0
- interpreto/attributions/metrics/__init__.py +25 -0
- interpreto/attributions/metrics/insertion_deletion.py +279 -0
- interpreto/attributions/perturbations/__init__.py +33 -0
- interpreto/attributions/perturbations/base.py +312 -0
- interpreto/attributions/perturbations/gaussian_noise_perturbation.py +81 -0
- interpreto/attributions/perturbations/gradient_shap_perturbation.py +81 -0
- interpreto/attributions/perturbations/insertion_deletion_perturbation.py +163 -0
- interpreto/attributions/perturbations/linear_interpolation_perturbation.py +134 -0
- interpreto/attributions/perturbations/occlusion_perturbation.py +82 -0
- interpreto/attributions/perturbations/random_perturbation.py +91 -0
- interpreto/attributions/perturbations/shap_perturbation.py +122 -0
- interpreto/attributions/perturbations/sobol_perturbation.py +137 -0
- interpreto/attributions/plots/__init__.py +0 -0
- interpreto/commons/__init__.py +28 -0
- interpreto/commons/distances.py +271 -0
- interpreto/commons/generator_tools.py +168 -0
- interpreto/commons/granularity.py +653 -0
- interpreto/concepts/__init__.py +67 -0
- interpreto/concepts/base.py +495 -0
- interpreto/concepts/interpretations/__init__.py +29 -0
- interpreto/concepts/interpretations/base.py +554 -0
- interpreto/concepts/interpretations/llm_labels.py +508 -0
- interpreto/concepts/interpretations/topk_inputs.py +335 -0
- interpreto/concepts/methods/__init__.py +71 -0
- interpreto/concepts/methods/cockatiel.py +91 -0
- interpreto/concepts/methods/neurons_as_concepts.py +152 -0
- interpreto/concepts/methods/overcomplete.py +722 -0
- interpreto/concepts/methods/sklearn_wrappers.py +420 -0
- interpreto/concepts/metrics/__init__.py +45 -0
- interpreto/concepts/metrics/consim.py +1335 -0
- interpreto/concepts/metrics/dictionary_metrics.py +198 -0
- interpreto/concepts/metrics/reconstruction_metrics.py +161 -0
- interpreto/concepts/metrics/sparsity_metrics.py +96 -0
- interpreto/concepts/plots/__init__.py +0 -0
- interpreto/model_wrapping/__init__.py +27 -0
- interpreto/model_wrapping/classification_inference_wrapper.py +227 -0
- interpreto/model_wrapping/generation_inference_wrapper.py +214 -0
- interpreto/model_wrapping/inference_wrapper.py +693 -0
- interpreto/model_wrapping/llm_interface.py +149 -0
- interpreto/model_wrapping/model_with_split_points.py +1408 -0
- interpreto/model_wrapping/splitting_utils.py +125 -0
- interpreto/model_wrapping/transformers_classes.py +88 -0
- interpreto/typing.py +98 -0
- interpreto/visualizations/__init__.py +0 -0
- interpreto/visualizations/attributions/__init__.py +5 -0
- interpreto/visualizations/attributions/attribution_highlight.py +323 -0
- interpreto/visualizations/base.py +237 -0
- interpreto/visualizations/colormap_helpers.py +59 -0
- interpreto/visualizations/concepts/__init__.py +5 -0
- interpreto/visualizations/concepts/concepts_highlight.py +176 -0
- interpreto/visualizations/visualization.css +129 -0
- interpreto/visualizations/visualization.js +606 -0
- interpreto/visualizations/visualization_attribution.js +591 -0
- interpreto-0.4.9.dist-info/METADATA +279 -0
- interpreto-0.4.9.dist-info/RECORD +76 -0
- interpreto-0.4.9.dist-info/WHEEL +5 -0
- interpreto-0.4.9.dist-info/licenses/LICENSE +23 -0
- 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
|
+
)
|