Skip to content

Commit 34d43b2

Browse files
author
Ashley Scillitoe
committed
Merge branch 'master' into dependabot/pip/sphinx-gte-4.2.0-and-lt-7.0.0
2 parents 2402306 + f2bcd4e commit 34d43b2

File tree

16 files changed

+955
-277
lines changed

16 files changed

+955
-277
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,12 @@ jobs:
8383
pytest -m "not tf1" alibi
8484
8585
- name: Upload coverage to Codecov
86-
if: ${{ success() }}
87-
run: |
88-
codecov -F ${{ matrix.os }}-${{ matrix.python-version }}
86+
uses: codecov/codecov-action@v3
87+
with:
88+
directory: .
89+
env_vars: ${{matrix.os}}, ${{matrix.python-version}}
90+
fail_ci_if_error: false
91+
verbose: true
8992

9093
- name: Build Python package
9194
run: |

alibi/explainers/anchors/anchor_tabular.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,9 +912,10 @@ def add_names_to_exp(self, explanation: dict) -> None:
912912
"""
913913

914914
anchor_idxs = explanation['feature']
915-
explanation['names'] = []
916-
explanation['feature'] = [self.enc2feat_idx[idx] for idx in anchor_idxs]
917915
ordinal_ranges = {self.enc2feat_idx[idx]: [float('-inf'), float('inf')] for idx in anchor_idxs}
916+
explanation['feature'] = list(ordinal_ranges.keys())
917+
explanation['names'] = []
918+
918919
for idx in set(anchor_idxs) - self.cat_lookup.keys():
919920
feat_id = self.enc2feat_idx[idx] # feature col. id
920921
if 0 in self.ord_lookup[idx]: # tells if the feature in X falls in a higher or lower bin

alibi/explainers/partial_dependence.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,17 @@ def _build_explanation(self,
506506
)
507507
return Explanation(meta=copy.deepcopy(self.meta), data=data)
508508

509+
def reset_predictor(self, predictor: Union[Callable[[np.ndarray], np.ndarray], BaseEstimator]) -> None:
510+
"""
511+
Resets the predictor function or tree-based `sklearn` estimator.
512+
513+
Parameters
514+
----------
515+
predictor
516+
New predictor function or tree-based `sklearn` estimator.
517+
"""
518+
self.predictor = predictor
519+
509520

510521
class PartialDependence(PartialDependenceBase):
511522
""" Black-box implementation of partial dependence for tabular datasets.

alibi/explainers/similarity/backends/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
from typing import Union, Type
1+
from typing import Type, Union
22

3-
from alibi.utils.frameworks import has_pytorch, has_tensorflow, Framework
3+
from alibi.utils.frameworks import Framework, has_pytorch, has_tensorflow
44

55
if has_pytorch:
66
# import pytorch backend
7-
from alibi.explainers.similarity.backends.pytorch.base import _PytorchBackend
7+
from alibi.explainers.similarity.backends.pytorch.base import \
8+
_PytorchBackend
89

910
if has_tensorflow:
1011
# import tensorflow backend
11-
from alibi.explainers.similarity.backends.tensorflow.base import _TensorFlowBackend
12+
from alibi.explainers.similarity.backends.tensorflow.base import \
13+
_TensorFlowBackend
1214

1315

1416
def _select_backend(backend: Framework = Framework.TENSORFLOW) \

alibi/explainers/similarity/backends/pytorch/base.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
backend in order to ensure that the similarity methods only require to match this interface.
55
"""
66

7-
from typing import Callable, Union, Optional
7+
from typing import Any, Callable, List, Optional, Union
88

99
import numpy as np
10-
import torch.nn as nn
1110
import torch
11+
import torch.nn as nn
1212

1313

1414
class _PytorchBackend:
@@ -17,7 +17,7 @@ class _PytorchBackend:
1717
@staticmethod
1818
def get_grads(
1919
model: nn.Module,
20-
X: torch.Tensor,
20+
X: Union[torch.Tensor, List[Any]],
2121
Y: torch.Tensor,
2222
loss_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor],
2323
) -> np.ndarray:

alibi/explainers/similarity/backends/tensorflow/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
backend in order to ensure that the similarity methods only require to match this interface.
55
"""
66

7-
from typing import Callable, Optional, Union
7+
from typing import Any, Callable, List, Optional, Union
88

99
import numpy as np
1010
import tensorflow as tf
@@ -17,7 +17,7 @@ class _TensorFlowBackend:
1717
@staticmethod
1818
def get_grads(
1919
model: keras.Model,
20-
X: tf.Tensor,
20+
X: Union[tf.Tensor, List[Any]],
2121
Y: tf.Tensor,
2222
loss_fn: Callable[[tf.Tensor, tf.Tensor], tf.Tensor],
2323
) -> np.ndarray:

alibi/explainers/similarity/base.py

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
from abc import ABC
2-
from typing import TYPE_CHECKING, Callable, Union, Tuple, Optional
3-
from typing_extensions import Literal
2+
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union
43

54
import numpy as np
6-
from tqdm import tqdm
7-
85
from alibi.api.interfaces import Explainer
96
from alibi.explainers.similarity.backends import _select_backend
10-
from alibi.utils.frameworks import Framework
7+
from alibi.utils.frameworks import Framework, has_pytorch, has_tensorflow
8+
from alibi.utils.missing_optional_dependency import import_optional
9+
from tqdm import tqdm
10+
from typing_extensions import Literal
11+
12+
_TfTensor = import_optional('tensorflow', ['Tensor'])
13+
_PtTensor = import_optional('torch', ['Tensor'])
1114

1215
if TYPE_CHECKING:
1316
import tensorflow
@@ -63,7 +66,7 @@ def __init__(self,
6366
super().__init__(meta=meta)
6467

6568
def fit(self,
66-
X_train: np.ndarray,
69+
X_train: Union[np.ndarray, List[Any]],
6770
Y_train: np.ndarray) -> "Explainer":
6871
"""Fit the explainer. If ``self.precompute_grads == True`` then the gradients are precomputed and stored.
6972
@@ -79,21 +82,42 @@ def fit(self,
7982
self
8083
Returns self.
8184
"""
82-
self.X_train: np.ndarray = X_train
83-
self.Y_train: np.ndarray = Y_train
84-
self.X_dims: Tuple = self.X_train.shape[1:]
85-
self.Y_dims: Tuple = self.Y_train.shape[1:]
86-
self.grad_X_train: np.ndarray = np.array([])
85+
self.X_train = X_train
86+
self.Y_train = Y_train
87+
self.X_dims = self.X_train.shape[1:] if isinstance(self.X_train, np.ndarray) else None
88+
self.Y_dims = self.Y_train.shape[1:]
89+
self.grad_X_train = np.array([])
8790

8891
# compute and store gradients
8992
if self.precompute_grads:
9093
grads = []
94+
X: Union[np.ndarray, List[Any]]
9195
for X, Y in tqdm(zip(self.X_train, self.Y_train), disable=not self.verbose):
92-
grad_X_train = self._compute_grad(X[None], Y[None])
96+
grad_X_train = self._compute_grad(self._format(X), Y[None])
9397
grads.append(grad_X_train[None])
98+
9499
self.grad_X_train = np.concatenate(grads, axis=0)
95100
return self
96101

102+
@staticmethod
103+
def _is_tensor(x: Any) -> bool:
104+
"""Checks if an obejct is a tensor."""
105+
if has_tensorflow and isinstance(x, _TfTensor):
106+
return True
107+
if has_pytorch and isinstance(x, _PtTensor):
108+
return True
109+
if isinstance(x, np.ndarray):
110+
return True
111+
return False
112+
113+
@staticmethod
114+
def _format(x: 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor, Any]'
115+
) -> 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor, List[Any]]':
116+
"""Adds batch dimension."""
117+
if BaseSimilarityExplainer._is_tensor(x):
118+
return x[None]
119+
return [x]
120+
97121
def _verify_fit(self) -> None:
98122
"""Verify that the explainer has been fitted.
99123
@@ -102,14 +126,15 @@ def _verify_fit(self) -> None:
102126
ValueError
103127
If the explainer has not been fitted.
104128
"""
105-
106129
if not hasattr(self, 'X_train') or not hasattr(self, 'Y_train'):
107130
raise ValueError('Training data not set. Call `fit` and pass training data first.')
108131

109132
def _match_shape_to_data(self,
110-
data: 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor]',
111-
target_type: Literal['X', 'Y']) -> 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor]':
112-
"""Verify the shape of `data` against the shape of the training data.
133+
data: 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor, Any, List[Any]]',
134+
target_type: Literal['X', 'Y']
135+
) -> 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor, List[Any]]':
136+
"""
137+
Verify the shape of `data` against the shape of the training data.
113138
114139
Used to ensure input is correct shape for gradient methods implemented in the backends. `data` will be the
115140
features or label of the instance being explained. If the `data` is not a batch, reshape to be a single batch
@@ -131,6 +156,15 @@ def _match_shape_to_data(self,
131156
If the shape of `data` does not match the shape of the training data, or fit has not been called prior to
132157
calling this method.
133158
"""
159+
if self._is_tensor(data):
160+
return self._match_shape_to_data_tensor(data, target_type)
161+
return self._match_shape_to_data_any(data)
162+
163+
def _match_shape_to_data_tensor(self,
164+
data: 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor]',
165+
target_type: Literal['X', 'Y']
166+
) -> 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor]':
167+
""" Verify the shape of `data` against the shape of the training data for tensor like data."""
134168
target_shape = getattr(self, f'{target_type}_dims')
135169
if data.shape == target_shape:
136170
data = data[None]
@@ -139,6 +173,13 @@ def _match_shape_to_data(self,
139173
f' but training data has shape {target_shape}'))
140174
return data
141175

176+
@staticmethod
177+
def _match_shape_to_data_any(data: Union[Any, List[Any]]) -> list:
178+
""" Ensures that any other data type is a list."""
179+
if isinstance(data, list):
180+
return data
181+
return [data]
182+
142183
def _compute_adhoc_similarity(self, grad_X: np.ndarray) -> np.ndarray:
143184
"""
144185
Computes the similarity between the gradients of the test instances and all the training instances. The method
@@ -149,18 +190,18 @@ def _compute_adhoc_similarity(self, grad_X: np.ndarray) -> np.ndarray:
149190
grad_X
150191
Gradients of the test instances.
151192
"""
152-
scores = np.zeros((grad_X.shape[0], self.X_train.shape[0]))
193+
scores = np.zeros((len(grad_X), len(self.X_train)))
194+
X: Union[np.ndarray, List[Any]]
153195
for i, (X, Y) in tqdm(enumerate(zip(self.X_train, self.Y_train)), disable=not self.verbose):
154-
grad_X_train = self._compute_grad(X[None], Y[None])
196+
grad_X_train = self._compute_grad(self._format(X), Y[None])
155197
scores[:, i] = self.sim_fn(grad_X, grad_X_train[None])[:, 0]
156198
return scores
157199

158200
def _compute_grad(self,
159-
X: 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor]',
201+
X: 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor, List[Any]]',
160202
Y: 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor]') \
161203
-> np.ndarray:
162204
"""Computes predictor parameter gradients and returns a flattened `numpy` array."""
163-
164205
X = self.backend.to_tensor(X) if isinstance(X, np.ndarray) else X
165206
Y = self.backend.to_tensor(Y) if isinstance(Y, np.ndarray) else Y
166207
return self.backend.get_grads(self.predictor, X, Y, self.loss_fn)

alibi/explainers/similarity/grad.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,19 @@
44
"""
55

66
import copy
7-
from typing import TYPE_CHECKING, Callable, Optional, Union, Dict, Tuple
8-
from typing_extensions import Literal
9-
from enum import Enum
107
import warnings
8+
from enum import Enum
9+
from typing import (TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple,
10+
Union)
1111

1212
import numpy as np
13-
14-
from alibi.api.interfaces import Explanation
13+
from alibi.api.defaults import DEFAULT_DATA_SIM, DEFAULT_META_SIM
14+
from alibi.api.interfaces import Explainer, Explanation
1515
from alibi.explainers.similarity.base import BaseSimilarityExplainer
16-
from alibi.explainers.similarity.metrics import dot, cos, asym_dot
17-
from alibi.api.defaults import DEFAULT_META_SIM, DEFAULT_DATA_SIM
16+
from alibi.explainers.similarity.metrics import asym_dot, cos, dot
1817
from alibi.utils import _get_options_string
1918
from alibi.utils.frameworks import Framework
20-
from alibi.api.interfaces import Explainer
19+
from typing_extensions import Literal
2120

2221
if TYPE_CHECKING:
2322
import tensorflow
@@ -140,7 +139,7 @@ def __init__(self,
140139
warnings.warn(warning_msg)
141140

142141
def fit(self,
143-
X_train: np.ndarray,
142+
X_train: Union[np.ndarray, List[Any]],
144143
Y_train: np.ndarray) -> "Explainer":
145144
"""Fit the explainer.
146145
@@ -165,7 +164,7 @@ def fit(self,
165164

166165
def _preprocess_args(
167166
self,
168-
X: 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor]',
167+
X: 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor, Any, List[Any]]',
169168
Y: 'Optional[Union[np.ndarray, tensorflow.Tensor, torch.Tensor]]' = None) \
170169
-> 'Union[Tuple[torch.Tensor, torch.Tensor], Tuple[tensorflow.Tensor, tensorflow.Tensor]]':
171170
"""Formats `X`, `Y` for explain method.
@@ -205,7 +204,7 @@ def _preprocess_args(
205204

206205
def explain(
207206
self,
208-
X: 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor]',
207+
X: 'Union[np.ndarray, tensorflow.Tensor, torch.Tensor, Any, List[Any]]',
209208
Y: 'Optional[Union[np.ndarray, tensorflow.Tensor, torch.Tensor]]' = None) -> "Explanation":
210209
"""Explain the predictor's predictions for a given input.
211210
@@ -217,8 +216,9 @@ def explain(
217216
Parameters
218217
----------
219218
X
220-
`X` can be a `numpy` array, `tensorflow` tensor, or `pytorch` tensor of the same shape as the training data
221-
with or without a leading batch dimension. If the batch dimension is missing it's added.
219+
`X` can be a `numpy` array, `tensorflow` tensor, `pytorch` tensor of the same shape as the training data
220+
or a list of objects, with or without a leading batch dimension. If the batch dimension is missing it's
221+
added.
222222
Y
223223
`Y` can be a `numpy` array, `tensorflow` tensor or a `pytorch` tensor. In the case of a regression task, the
224224
`Y` argument must be present. If the task is classification then `Y` defaults to the model prediction.
@@ -249,7 +249,7 @@ def explain(
249249
X, Y = self._preprocess_args(X, Y)
250250
test_grads = []
251251
for x, y in zip(X, Y):
252-
test_grads.append(self._compute_grad(x[None], y[None])[None])
252+
test_grads.append(self._compute_grad(self._format(x), y[None])[None])
253253
grads_X_test = np.concatenate(np.array(test_grads), axis=0)
254254
if not self.precompute_grads:
255255
scores = self._compute_adhoc_similarity(grads_X_test)
@@ -267,11 +267,24 @@ def _build_explanation(self, scores: np.ndarray) -> "Explanation":
267267
"""
268268
data = copy.deepcopy(DEFAULT_DATA_SIM)
269269
sorted_score_indices = np.argsort(scores)[:, ::-1]
270-
broadcast_indices = np.expand_dims(sorted_score_indices, axis=tuple(range(2, len(self.X_train[None].shape))))
270+
most_similar: Union[np.ndarray, List[Any]]
271+
least_similar: Union[np.ndarray, List[Any]]
272+
273+
if isinstance(self.X_train, np.ndarray):
274+
broadcast_indices = np.expand_dims(
275+
sorted_score_indices,
276+
axis=tuple(range(2, len(self.X_train[None].shape)))
277+
)
278+
most_similar = np.take_along_axis(self.X_train[None], broadcast_indices[:, :5], axis=1)
279+
least_similar = np.take_along_axis(self.X_train[None], broadcast_indices[:, -1:-6:-1], axis=1)
280+
else:
281+
most_similar = [[self.X_train[i] for i in ssi[:5]] for ssi in sorted_score_indices]
282+
least_similar = [[self.X_train[i] for i in ssi[-1:-6:-1]] for ssi in sorted_score_indices]
283+
271284
data.update(
272285
scores=np.take_along_axis(scores, sorted_score_indices, axis=1),
273286
ordered_indices=sorted_score_indices,
274-
most_similar=np.take_along_axis(self.X_train[None], broadcast_indices[:, :5], axis=1),
275-
least_similar=np.take_along_axis(self.X_train[None], broadcast_indices[:, -1:-6:-1], axis=1),
287+
most_similar=most_similar,
288+
least_similar=least_similar
276289
)
277290
return Explanation(meta=self.meta, data=data)

doc/source/examples/cem_iris.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@
448448
"pp['species'] = 'PP_' + class_names[expl['PP_pred']]\n",
449449
"orig_inst = pd.DataFrame(explanation.X, columns=dataset.feature_names)\n",
450450
"orig_inst['species'] = 'orig_' + class_names[explanation.X_pred]\n",
451-
"df = df.append([pn, pp, orig_inst], ignore_index=True)"
451+
"df = pd.concat([df, pn, pp, orig_inst], ignore_index=True)"
452452
]
453453
},
454454
{
@@ -634,7 +634,7 @@
634634
],
635635
"metadata": {
636636
"kernelspec": {
637-
"display_name": "Python 3",
637+
"display_name": "Python 3 (ipykernel)",
638638
"language": "python",
639639
"name": "python3"
640640
},
@@ -648,7 +648,7 @@
648648
"name": "python",
649649
"nbconvert_exporter": "python",
650650
"pygments_lexer": "ipython3",
651-
"version": "3.8.13"
651+
"version": "3.10.9"
652652
}
653653
},
654654
"nbformat": 4,

0 commit comments

Comments
 (0)