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
interpreto/__init__.py ADDED
@@ -0,0 +1,80 @@
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
+ # Copyright IRT Antoine de Saint Exupéry et Université Paul Sabatier Toulouse III - All
26
+ # rights reserved. DEEL and FOR are research programs operated by IVADO, IRT Saint Exupéry,
27
+ # CRIAQ and ANITI - https://www.deel.ai/
28
+ # =====================================================================================
29
+
30
+ from .attributions import (
31
+ GradientShap,
32
+ IntegratedGradients,
33
+ KernelShap,
34
+ Lime,
35
+ Occlusion,
36
+ Saliency,
37
+ SmoothGrad,
38
+ Sobol,
39
+ SquareGrad,
40
+ VarGrad,
41
+ )
42
+ from .commons import (
43
+ Granularity,
44
+ )
45
+ from .model_wrapping import ModelWithSplitPoints
46
+ from .visualizations.attributions import (
47
+ AttributionVisualization,
48
+ )
49
+ from .visualizations.concepts import (
50
+ ConceptHighlightVisualization,
51
+ )
52
+
53
+
54
+ def get_version() -> str:
55
+ """Returns the current version of the Interpreto library."""
56
+ import pkg_resources # noqa: PLC0415 # ruff: disable=import-outside-toplevel
57
+
58
+ try:
59
+ return pkg_resources.get_distribution("interpreto").version
60
+ except pkg_resources.DistributionNotFound:
61
+ return "unknown"
62
+
63
+
64
+ __all__ = [
65
+ "AttributionVisualization",
66
+ "ConceptHighlightVisualization",
67
+ "GradientShap",
68
+ "Granularity",
69
+ "IntegratedGradients",
70
+ "KernelShap",
71
+ "Lime",
72
+ "ModelWithSplitPoints",
73
+ "Occlusion",
74
+ "SquareGrad",
75
+ "Saliency",
76
+ "SmoothGrad",
77
+ "Sobol",
78
+ "VarGrad",
79
+ "get_version",
80
+ ]
@@ -0,0 +1,50 @@
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
+ from .base import InferenceModes
26
+ from .methods import (
27
+ GradientShap,
28
+ IntegratedGradients,
29
+ KernelShap,
30
+ Lime,
31
+ Occlusion,
32
+ Saliency,
33
+ SmoothGrad,
34
+ Sobol,
35
+ SquareGrad,
36
+ VarGrad,
37
+ )
38
+
39
+ __all__ = [
40
+ "GradientShap",
41
+ "IntegratedGradients",
42
+ "KernelShap",
43
+ "Lime",
44
+ "Occlusion",
45
+ "Saliency",
46
+ "SmoothGrad",
47
+ "Sobol",
48
+ "SquareGrad",
49
+ "VarGrad",
50
+ ]
@@ -0,0 +1,36 @@
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
+ from .base import (
26
+ Aggregator,
27
+ MaskwiseMeanAggregator,
28
+ MeanAggregator,
29
+ OcclusionAggregator,
30
+ SquaredMeanAggregator,
31
+ SumAggregator,
32
+ TrapezoidalMeanAggregator,
33
+ VarianceAggregator,
34
+ )
35
+ from .linear_regression_aggregation import LinearRegressionAggregator
36
+ from .sobol_aggregation import SobolAggregator
@@ -0,0 +1,159 @@
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
+ Aggregations used at the end of an attribution method
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from collections.abc import Callable
32
+
33
+ import torch
34
+ from beartype import beartype
35
+ from jaxtyping import Float, jaxtyped
36
+ from torch import Tensor
37
+
38
+
39
+ class Aggregator:
40
+ """
41
+ Abstract class for aggregation made at the end of attribution methods
42
+ """
43
+
44
+ def aggregate(self, results: torch.Tensor, mask) -> torch.Tensor:
45
+ """
46
+ Get results from multiple "Inference wrappers", aggregate results and gives an explanation
47
+ """
48
+ return results.squeeze(0)
49
+
50
+ def __call__(self, results: torch.Tensor, mask: torch.Tensor | None) -> torch.Tensor:
51
+ return self.aggregate(results, mask)
52
+
53
+
54
+ class TorchAggregator(Aggregator):
55
+ """
56
+ Basic aggregator using built-in torch methods to perform aggregation
57
+ """
58
+
59
+ _method: Callable[[torch.Tensor, int], torch.Tensor]
60
+
61
+ @jaxtyped(typechecker=beartype)
62
+ def aggregate(
63
+ self,
64
+ results: Float[Tensor, "p t l"],
65
+ mask: torch.Tensor | None = None,
66
+ ) -> Float[Tensor, "t l"]:
67
+ return self._method(results, dim=0) # type: ignore
68
+
69
+
70
+ class MeanAggregator(TorchAggregator):
71
+ """
72
+ Mean of attributions
73
+ """
74
+
75
+ _method = torch.mean
76
+
77
+
78
+ class SquaredMeanAggregator(TorchAggregator):
79
+ """
80
+ Mean of squares of attributions
81
+ """
82
+
83
+ @staticmethod
84
+ def _method(x: torch.Tensor, dim: int = 0) -> torch.Tensor: # type: ignore
85
+ return torch.mean(torch.square(x), dim=dim)
86
+
87
+
88
+ class SumAggregator(TorchAggregator):
89
+ """
90
+ Sum of attributions
91
+ """
92
+
93
+ _method = torch.sum
94
+
95
+
96
+ class VarianceAggregator(TorchAggregator):
97
+ """
98
+ Variance of attributions
99
+ """
100
+
101
+ _method = torch.var
102
+
103
+
104
+ class MaskwiseMeanAggregator(Aggregator):
105
+ """Average scores for each target weighted by their perturbation mask.
106
+
107
+ This aggregator assumes that ``results`` contains one score per
108
+ perturbation and target and that ``mask`` indicates which input elements were
109
+ perturbed. The returned tensor has one importance score per target and input
110
+ element.
111
+ """
112
+
113
+ @jaxtyped(typechecker=beartype)
114
+ def aggregate(
115
+ self,
116
+ results: Float[Tensor, "p t"],
117
+ mask: Float[Tensor, "p l"],
118
+ ) -> Float[Tensor, "t l"]:
119
+ return torch.einsum("pt,pl->tl", results, mask) / mask.sum(dim=0)
120
+
121
+
122
+ class OcclusionAggregator(Aggregator):
123
+ """Aggregate occlusion scores using a reference prediction.
124
+
125
+ The first row in ``results`` must correspond to the unperturbed prediction.
126
+ Subsequent rows represent the prediction for each perturbation. The returned
127
+ tensor contains the mean difference between the reference and each
128
+ perturbation weighted by the corresponding mask.
129
+ """
130
+
131
+ @jaxtyped(typechecker=beartype)
132
+ def aggregate(
133
+ self,
134
+ results: Float[Tensor, "p t"],
135
+ mask: Float[Tensor, "p l"],
136
+ ) -> Float[Tensor, "t l"]:
137
+ # first prediction is reference, unmodified input
138
+ scores = results[..., 0, :] - results[..., 1:, :]
139
+ mask = mask[..., 1:, :]
140
+ return torch.einsum("pt,pl->tl", scores, mask) / mask.sum(dim=0)
141
+
142
+
143
+ class TrapezoidalMeanAggregator(Aggregator):
144
+ """
145
+ Weighted mean using the trapezoidal rule.
146
+
147
+ This aggregator performs a weighted average across the first axis of the input tensor,
148
+ assigning a weight of 0.5 to the first and last elements, and 1.0 to all intermediate ones.
149
+
150
+ This reflects trapezoidal numerical integration over uniformly spaced samples.
151
+ """
152
+
153
+ @jaxtyped(typechecker=beartype)
154
+ def aggregate(
155
+ self,
156
+ results: Float[Tensor, "p t l"],
157
+ mask: torch.Tensor | None = None,
158
+ ) -> Float[Tensor, "t l"]:
159
+ return torch.trapezoid(results, dim=0) / (results.shape[0] - 1)
@@ -0,0 +1,194 @@
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
+ Aggregator for LIME and KernelSHAP
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from collections.abc import Callable
32
+ from enum import Enum
33
+
34
+ import torch
35
+ from beartype import beartype
36
+ from jaxtyping import Float, jaxtyped
37
+ from torch import Tensor
38
+ from torch.nn import functional as F
39
+
40
+ from interpreto.attributions.aggregations.base import Aggregator
41
+
42
+ DistancesFromMaskProtocol = Callable[[Float[Tensor, "p l"]], Float[Tensor, "p"]]
43
+ SimilarityKernelProtocol = Callable[[Float[Tensor, "p"], float], Float[Tensor, "p"]]
44
+
45
+
46
+ def hamming_distance(mask: torch.Tensor) -> torch.Tensor:
47
+ """
48
+ Compute the Hamming distance between a mask and the original input
49
+ """
50
+ return torch.sum(mask, dim=1).float()
51
+
52
+
53
+ def euclidean_distance(mask: torch.Tensor) -> torch.Tensor:
54
+ """
55
+ Compute the Euclidean distance between a mask and the original input
56
+ """
57
+ return torch.norm(mask, dim=1)
58
+
59
+
60
+ def cosine_distance(mask: torch.Tensor) -> torch.Tensor:
61
+ """
62
+ Compute the cosine distance between a mask and the original input
63
+ """
64
+ return 1 - F.cosine_similarity(torch.ones_like(mask), mask, dim=1)
65
+
66
+
67
+ class DistancesFromMask(Enum):
68
+ """
69
+ Enumeration of available distance functions.
70
+ """
71
+
72
+ HAMMING = staticmethod(hamming_distance)
73
+ EUCLIDEAN = staticmethod(euclidean_distance)
74
+ COSINE = staticmethod(cosine_distance)
75
+
76
+
77
+ def exponential_kernel(distances: torch.Tensor, kernel_width: float) -> torch.Tensor:
78
+ """
79
+ Compute the exponential kernel for a given distance matrix
80
+ """
81
+ return torch.exp(-(distances**2) / (kernel_width**2))
82
+
83
+
84
+ def ones_kernel(distances: torch.Tensor, kernel_width: float) -> torch.Tensor:
85
+ """
86
+ Compute the ones kernel for a given distance matrix
87
+ """
88
+ return torch.ones_like(distances)
89
+
90
+
91
+ class Kernels(Enum):
92
+ """
93
+ Enumeration of available kernels.
94
+ """
95
+
96
+ EXPONENTIAL = staticmethod(exponential_kernel)
97
+ ONES = staticmethod(ones_kernel)
98
+
99
+
100
+ def default_kernel_width_fn(mask: torch.Tensor) -> float:
101
+ """
102
+ Compute the default kernel width for a given mask
103
+ """
104
+ return (mask.shape[1] ** 0.5) * 0.75 # kernel parameter inspired by LIME
105
+
106
+
107
+ class LinearRegressionAggregator(Aggregator):
108
+ """
109
+ Aggregator for masked perturbations using a linear model
110
+ """
111
+
112
+ def __init__(
113
+ self,
114
+ distance_function: DistancesFromMaskProtocol | None = None, # noqa: UP036
115
+ similarity_kernel: SimilarityKernelProtocol = exponential_kernel,
116
+ kernel_width: float | Callable | None = None,
117
+ ):
118
+ """
119
+ Initialize the aggregator.
120
+
121
+ Args:
122
+ distance_function (DistancesFromMaskProtocol): distance function used to compute the similarity between perturbations and the original input from the mask.
123
+ similarity_kernel (Callable): similarity kernel used to compute the similarity between perturbations and the original input
124
+ kernel_width (float | Callable): kernel width used to compute the similarity between perturbations and the original input
125
+ """
126
+ self.distance_function = distance_function
127
+ self.similarity_kernel = similarity_kernel
128
+ self.kernel_width = kernel_width if kernel_width is not None else default_kernel_width_fn
129
+
130
+ @jaxtyped(typechecker=beartype)
131
+ def aggregate(
132
+ self,
133
+ results: Float[Tensor, "p t"],
134
+ mask: Float[Tensor, "p l"],
135
+ ) -> Float[Tensor, "t l"]:
136
+ """
137
+ Aggregate the results of the perturbations using a linear model.
138
+ We use the closed form solution for the weighted least squares problem.
139
+
140
+ Args:
141
+ results (torch.Tensor): The results of the perturbations. Shape: (p, t).
142
+ p the number of perturbations
143
+ t the number of target classes/tokens
144
+ mask (torch.Tensor): The mask used for the perturbations. Shape: (p, l).
145
+ l the length of the sequence
146
+ Returns:
147
+ torch.Tensor: The aggregated results. Shape: (t, l).
148
+ """
149
+ # Simplify typing
150
+ p, l = mask.shape
151
+ t = results.shape[1]
152
+
153
+ if results.shape[0] != p:
154
+ raise ValueError(
155
+ "The number of perturbations must match the number of results."
156
+ + f"Got {results.shape} perturbations and {results.shape} results."
157
+ )
158
+
159
+ # Define the weights for the linear model
160
+ if self.distance_function is not None: # LIME
161
+ # Compute distance between perturbations and original input using the mask
162
+ distances: Float[Tensor, p] = self.distance_function(mask)
163
+
164
+ # Compute the similarities between perturbations and original input using the distances
165
+ if isinstance(self.kernel_width, Callable):
166
+ kernel_width: float = self.kernel_width(mask)
167
+ else:
168
+ assert isinstance(self.kernel_width, float) or isinstance(self.kernel_width, int)
169
+ kernel_width: float = self.kernel_width
170
+ weights: Float[Tensor, p] = self.similarity_kernel(distances, kernel_width)
171
+ else: # Kernel SHAP
172
+ weights: Float[Tensor, p] = self.similarity_kernel(mask[:, 0], 1.0)
173
+
174
+ # We take the opposite of the mask
175
+ # Otherwise the linear treat the perturbation of a feature as passing from 0 to 1 for this feature
176
+ # We want exactly the opposite
177
+ mask = 1 - mask
178
+
179
+ # Compute closed form solution for the linear model
180
+ # Add a constant column for the bias term
181
+ X: Float[Tensor, p, l + 1] = torch.cat([torch.ones(results.shape[0], 1, device=mask.device), mask], dim=1)
182
+
183
+ # formula from wikipedia: https://en.wikipedia.org/wiki/Weighted_least_squares
184
+ # \theta =(X^T w X)^{-1} (X^T w y)
185
+ XT_W: Float[Tensor, l + 1, p] = X.T * weights
186
+ epsilon: Float[Tensor, l + 1, l + 1] = (
187
+ torch.eye(XT_W.shape[0], device=XT_W.device) * 1e-6
188
+ ) # To prevent inversion errors
189
+ theta: Float[Tensor, l + 1, t] = torch.inverse(XT_W @ X + epsilon) @ (XT_W @ results)
190
+
191
+ # exclude the bias term
192
+ token_importance: Float[Tensor, t, l] = theta[1:].T
193
+
194
+ return token_importance
@@ -0,0 +1,116 @@
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
+ from __future__ import annotations
26
+
27
+ from enum import Enum
28
+
29
+ import torch
30
+ from beartype import beartype
31
+ from jaxtyping import Float, jaxtyped
32
+
33
+ from interpreto.attributions.aggregations.base import Aggregator
34
+
35
+
36
+ class SobolIndicesOrders(Enum):
37
+ """
38
+ Enumeration of available Sobol indices orders.
39
+ """
40
+
41
+ FIRST_ORDER = "first order"
42
+ TOTAL_ORDER = "total order"
43
+
44
+
45
+ class SobolAggregator(Aggregator):
46
+ """
47
+ Aggregates Sobol indices from model outputs.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ n_token_perturbations: int,
53
+ sobol_indices_order: SobolIndicesOrders = SobolIndicesOrders.FIRST_ORDER,
54
+ ):
55
+ """
56
+ Initialize the aggregator.
57
+
58
+ Args:
59
+ n_token_perturbations (int): Number of token perturbations.
60
+ sobol_indices_order (SobolIndicesOrders): Sobol indices order, either `FIRST_ORDER` or `TOTAL_ORDER`.
61
+ """
62
+ self.n_token_perturbations = n_token_perturbations
63
+ self.sobol_indices_order = sobol_indices_order.value
64
+
65
+ @jaxtyped(typechecker=beartype)
66
+ def aggregate(self, results: Float[torch.Tensor, "p t"], mask: torch.Tensor) -> Float[torch.Tensor, "t l"]: # noqa: UP037
67
+ """
68
+ Compute the Sobol indices from the model outputs perturbed inputs.
69
+
70
+ Args:
71
+ results (torch.Tensor): The model outputs on perturbed inputs. Shape: (p, t) with p = (l + 2) * k
72
+ - l is the number of elements perturbed.
73
+ - k is the number of perturbations per element.
74
+ - t is the targets dimensions, i.e. the number of classes or number of generated tokens.
75
+ mask (torch.Tensor): Ignored.
76
+
77
+ Returns:
78
+ token_importance (torch.Tensor): The Sobol attribution indices for each token. Shape: (l,)
79
+ """
80
+ # simplify typing
81
+ k = self.n_token_perturbations
82
+ l = (results.shape[0] // k) - 2
83
+ t = results.shape[1]
84
+
85
+ # Extract initial matrices
86
+ fA: Float[torch.Tensor, k, t] = results[:k]
87
+ fB: Float[torch.Tensor, k, t] = results[k : 2 * k]
88
+ fC: Float[torch.Tensor, l, k, t] = results[2 * k :].view(l, k, t)
89
+
90
+ var_fA: Float[torch.Tensor, 1, t] = torch.var(fA, dim=0, keepdim=True)
91
+
92
+ token_importance: Float[torch.Tensor, l, t]
93
+ if self.sobol_indices_order == SobolIndicesOrders.FIRST_ORDER.value:
94
+ # (var(f(A)) - 0.5*mean(f(B) - f(C))^2) / var(f(A))
95
+ fBfC_diff: Float[torch.Tensor, l, k, t] = fB.unsqueeze(0) - fC
96
+ fBfC_mean_square_diff: Float[torch.Tensor, l, t] = torch.mean(fBfC_diff**2, dim=1) / 2
97
+ token_importance: Float[torch.Tensor, l, t] = (var_fA - fBfC_mean_square_diff) / (var_fA + 1e-6)
98
+ elif self.sobol_indices_order == SobolIndicesOrders.TOTAL_ORDER.value:
99
+ # mean(f(A) - f(C))^2) / 2var(f(A))
100
+ fAfC_diff: Float[torch.Tensor, l, k, t] = fA.unsqueeze(0) - fC
101
+ fAfC_mean_square_diff: Float[torch.Tensor, l, t] = torch.mean(fAfC_diff**2, dim=1)
102
+ token_importance: Float[torch.Tensor, l, t] = fAfC_mean_square_diff / (2 * var_fA + 1e-6)
103
+ else:
104
+ raise ValueError(f"Unknown Sobol indices order: {self.sobol_indices_order}")
105
+
106
+ return token_importance.T
107
+
108
+ # # Compute token-wise variance on the initial mask
109
+ # initial_results: Float[torch.Tensor, t, k] = results[:k].T
110
+ # initial_var: Float[torch.Tensor, t, 1] = torch.var(initial_results, dim=1, keepdim=True)
111
+
112
+ # # Compute token-wise sobol attribution indices
113
+ # token_results: Float[torch.Tensor, t, l, k] = results[k:].view(t, l, k)
114
+ # difference: Float[torch.Tensor, t, l, k] = token_results - initial_results.unsqueeze(1)
115
+ # token_importance: Float[torch.Tensor, t, l] = torch.mean(difference**2, dim=-1) / (initial_var + 1e-6)
116
+ # return token_importance