Skip to content

Commit

Permalink
Merge branch 'master' into python_3.10
Browse files Browse the repository at this point in the history
  • Loading branch information
ascillitoe committed Apr 26, 2022
2 parents abddbb9 + 6ce8a51 commit c44fc08
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 24 deletions.
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@

<!--- BADGES: START --->

<!---
![Python version](https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9%20%7C%203.10-blue.svg)
[![PyPI version](https://badge.fury.io/py/alibi-detect.svg)](https://badge.fury.io/py/alibi-detect)
--->
[![Build Status](https://github.com/SeldonIO/alibi-detect/workflows/CI/badge.svg?branch=master)][#build-status]
[![Documentation Status](https://readthedocs.org/projects/alibi/badge/?version=latest)][#docs-package]
[![codecov](https://codecov.io/gh/SeldonIO/alibi/branch/master/graph/badge.svg)](https://codecov.io/gh/SeldonIO/alibi)
Expand Down
114 changes: 95 additions & 19 deletions alibi/explainers/integrated_gradients.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def _compute_convergence_delta(model: Union[tf.keras.models.Model],
start_point: Union[List[np.ndarray], np.ndarray],
end_point: Union[List[np.ndarray], np.ndarray],
forward_kwargs: Optional[dict],
target: Optional[List[int]],
target: Optional[np.ndarray],
_is_list: bool) -> np.ndarray:
"""
Computes convergence deltas for each data point. Convergence delta measures how close the sum of all attributions
Expand Down Expand Up @@ -112,13 +112,17 @@ def _select_target(preds: tf.Tensor,
Selected predictions.
"""
if not isinstance(targets, tf.Tensor):
targets = tf.convert_to_tensor(targets)

if targets is not None:
if isinstance(preds, tf.Tensor):
preds = tf.linalg.diag_part(tf.gather(preds, targets, axis=1))
preds = tf.gather_nd(preds, tf.expand_dims(targets, axis=1), batch_dims=1)
else:
raise NotImplementedError
else:
raise ValueError("target cannot be `None` if `model` output dimensions > 1")

return preds


Expand Down Expand Up @@ -530,9 +534,9 @@ def _format_baseline(X: np.ndarray,


def _format_target(target: Union[None, int, list, np.ndarray],
nb_samples: int) -> Union[None, List[int]]:
nb_samples: int) -> Union[None, np.ndarray]:
"""
Formats target to return a list.
Formats target to return a np.array.
Parameters
----------
Expand All @@ -543,20 +547,87 @@ def _format_target(target: Union[None, int, list, np.ndarray],
Returns
-------
Formatted target as a list.
Formatted target as a np.array.
"""
if target is not None:
if isinstance(target, int):
target = [target for _ in range(nb_samples)]
elif isinstance(target, list) or isinstance(target, np.ndarray):
target = [t.astype(int) for t in target]
target = np.array([target for _ in range(nb_samples)])
elif isinstance(target, list):
target = np.array(target)
elif isinstance(target, np.ndarray):
pass
else:
raise NotImplementedError

return target


def _check_target(output_shape: Tuple,
target: Optional[np.ndarray],
nb_samples: int) -> None:
"""
Parameters
----------
output_shape
Output shape of the tensorflow model
target
Target formatted as np array target.
nb_samples
Number of samples in the batch.
Returns
-------
None
"""

if target is not None:

if not (target.dtype == int):
raise ValueError("Targets must be integers")

if target.shape[0] != nb_samples:
raise ValueError(f"First dimension in target must be the same as nb of samples. "
f"Found target first dimension: {target.shape[0]}; nb of samples: {nb_samples}")

if len(target.shape) > 2:
raise ValueError("Target must be a rank-1 or a rank-2 tensor. If target is a rank-2 tensor, "
"each column contains the index of the corresponding dimension "
"in the model's output tensor.")

if len(output_shape) == 1:
# in case of squash output, the rank of the model's output tensor (out_rank) consider the batch dimension
out_rank, target_rank = 1, len(target.shape)
tmax, tmin = target.max(axis=0), target.min(axis=0)

if tmax > 1:
raise ValueError(f"Target value {tmax} out of range for output shape {output_shape} ")

# for all other cases, batch dimension is not considered in the out_rank
elif len(output_shape) == 2:
out_rank, target_rank = 1, len(target.shape)
tmax, tmin = target.max(axis=0), target.min(axis=0)

if (output_shape[-1] > 1 and (tmax >= output_shape[-1]).any()) or (output_shape[-1] == 1 and tmax > 1):
raise ValueError(f"Target value {tmax} out of range for output shape {output_shape} ")

else:
out_rank, target_rank = len(output_shape[1:]), target.shape[-1]
tmax, tmin = target.max(axis=0), target.min(axis=0)

if (tmax >= output_shape[1:]).any():
raise ValueError(f"Target value {tmax} out of range for output shape {output_shape} ")

if (tmin < 0).any():
raise ValueError(f"Negative value {tmin} for target. Targets represent positional "
f"arguments and cannot be negative")

if out_rank != target_rank:
raise ValueError(f"The last dimension of target must match the rank of the model's output tensor. "
f"Found target last dimension: {target_rank}; model's output rank: {out_rank}")


def _get_target_from_target_fn(target_fn: Callable,
model: tf.keras.Model,
X: Union[np.ndarray, List[np.ndarray]],
Expand Down Expand Up @@ -598,7 +669,7 @@ def _get_target_from_target_fn(target_fn: Callable,
# TODO: in the future we want to support outputs that are >2D at which point this check should change
msg = f"`target_fn` returned an array of shape {target.shape} but expected an array of shape {expected_shape}."
raise ValueError(msg) # TODO: raise a more specific error type?
return target
return target.astype(int)


def _sum_integral_terms(step_sizes: list,
Expand Down Expand Up @@ -633,7 +704,7 @@ def _sum_integral_terms(step_sizes: list,

def _calculate_sum_int(batches: List[List[tf.Tensor]],
model: Union[tf.keras.Model],
target: Union[None, List[int]],
target: Optional[np.ndarray],
target_paths: np.ndarray,
n_steps: int,
nb_samples: int,
Expand Down Expand Up @@ -683,7 +754,7 @@ def _calculate_sum_int(batches: List[List[tf.Tensor]],


def _validate_output(model: tf.keras.Model,
target: Optional[List[int]]) -> None:
target: Optional[np.ndarray]) -> None:
"""
Validates the model's output type and raises an error if the output type is not supported.
Expand Down Expand Up @@ -804,11 +875,16 @@ def explain(self,
If not provided, all features values for the baselines are set to 0.
target
Defines which element of the model output is considered to compute the gradients.
It can be a list of integers or a numeric value. If a numeric value is passed, the gradients are calculated
for the same element of the output for all data points.
It must be provided if the model output dimension is higher than 1.
Target can be a numpy array, a list or a numeric value.
Numeric values are only valid if the model's output is a rank-n tensor
with n <= 2 (regression and classification models).
If a numeric value is passed, the gradients are calculated for
the same element of the output for all data points.
For regression models whose output is a scalar, target should not be provided.
For classification models `target` can be either the true classes or the classes predicted by the model.
It must be provided for classification models and regression models whose output is a vector.
If the model's output is a rank-n tensor with n > 2,
the target must be a rank-2 numpy array or a list of lists (a matrix) with dimensions nb_samples X (n-1) .
attribute_to_layer_inputs
In case of layers gradients, controls whether the gradients are computed for the layer's inputs or
outputs. If ``True``, gradients are computed for the layer's inputs, if ``False`` for the layer's outputs.
Expand Down Expand Up @@ -886,7 +962,7 @@ def explain(self,
self.model(inputs, **forward_kwargs)

_validate_output(self.model, target) # type: ignore[arg-type]

_check_target(self.model.output_shape, target, nb_samples)
if self.layer is None:
# No layer passed, attributions computed with respect to the inputs
attributions = self._compute_attributions_list_input(X,
Expand Down Expand Up @@ -934,7 +1010,7 @@ def explain(self,
self.model(inputs, **forward_kwargs)

_validate_output(self.model, target)

_check_target(self.model.output_shape, target, nb_samples)
if self.layer is None:
attributions = self._compute_attributions_tensor_input(X,
baselines,
Expand Down Expand Up @@ -996,7 +1072,7 @@ def _build_explanation(self,
X: Union[List[np.ndarray], np.ndarray],
forward_kwargs: Optional[dict],
baselines: List[np.ndarray],
target: Optional[List[int]],
target: Optional[np.ndarray],
attributions: Union[List[np.ndarray], List[tf.Tensor]],
deltas: np.ndarray) -> Explanation:
if forward_kwargs is None:
Expand Down Expand Up @@ -1030,7 +1106,7 @@ def reset_predictor(self, predictor: Union[tf.keras.Model]) -> None:
def _compute_attributions_list_input(self,
X: List[np.ndarray],
baselines: Union[List[int], List[float], List[np.ndarray]],
target: Optional[List[int]],
target: Optional[np.ndarray],
step_sizes: List[float],
alphas: List[float],
nb_samples: int,
Expand Down Expand Up @@ -1155,7 +1231,7 @@ def _compute_attributions_list_input(self,
def _compute_attributions_tensor_input(self,
X: Union[np.ndarray, tf.Tensor],
baselines: Union[np.ndarray, tf.Tensor],
target: Optional[List[int]],
target: Optional[np.ndarray],
step_sizes: List[float],
alphas: List[float],
nb_samples: int,
Expand Down
63 changes: 62 additions & 1 deletion alibi/explainers/tests/test_integrated_gradients.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
from alibi.explainers import IntegratedGradients
from alibi.explainers.integrated_gradients import (_get_target_from_target_fn,
_run_forward_from_layer,
_run_forward_to_layer)
_run_forward_to_layer,
_check_target,
_select_target)

# generate some dummy data
N = 100
Expand Down Expand Up @@ -650,6 +652,65 @@ def test_run_forward_from_layer(layer_nb,
assert np.allclose(preds_from_layer, expected_values)


TARGETS_ARGS = [{'output_shape': (None, 2), 'target': np.array([0, 2]), 'nb_samples': 2, 'bad_target': True},
{'output_shape': (None,), 'target': np.array([0]), 'nb_samples': 2, 'bad_target': True},
{'output_shape': (None, 3), 'target': np.array([0, 0, 0]), 'nb_samples': 2, 'bad_target': True},
{'output_shape': (None, 3), 'target': np.array([-123, -123]), 'nb_samples': 2, 'bad_target': True},
{'output_shape': (None, 3), 'target': np.array([99, 99, 99]), 'nb_samples': 2, 'bad_target': True},
{'output_shape': (None, 3), 'target': 999, 'nb_samples': 2, 'bad_target': True},
{'output_shape': (None, 2), 'target': np.array([[0, 2]]), 'nb_samples': 2, 'bad_target': True},
{'output_shape': (None, 4, 4, 3), 'target': np.array([[0, 0], [0, 0]]),
'nb_samples': 2, 'bad_target': True},
{'output_shape': (None, 4, 4, 3), 'target': np.array([[0, 0, 3], [0, 0, 0]]),
'nb_samples': 2, 'bad_target': True},
{'output_shape': (None, 4, 4, 3), 'target': np.array([[0, 4, 0], [0, 0, 0]]),
'nb_samples': 2, 'bad_target': True},
{'output_shape': (None, 4, 4, 3), 'target': np.array([[99, 99, 99], [0, 0, 0]]),
'nb_samples': 2, 'bad_target': True},
{'output_shape': (None, 2), 'target': np.array([0, 1]), 'nb_samples': 2, 'bad_target': False},
{'output_shape': (None,), 'target': np.array([0, 1]), 'nb_samples': 2, 'bad_target': False},
{'output_shape': (None, 3), 'target': np.array([0, 2]), 'nb_samples': 2, 'bad_target': False},
{'output_shape': (None, 4, 4, 3), 'target': np.array([[0, 0, 0], [3, 3, 2]]),
'nb_samples': 2, 'bad_target': False}]

SELECT_ARGS = [{'preds': np.array([[0.0, 0.1],
[1.0, 1.1]]),
'target': np.array([0,
0]),
'expected': np.array([0.0,
1.0])},
{'preds': np.array([[[0.0, 0.1], [1.0, 1.1]],
[[2.0, 2.1], [3.0, 4.1]]]),
'target': np.array([[0, 0],
[0, 0]]),
'expected': np.array([[0.0],
[2.0]])}]


@pytest.mark.parametrize('args', TARGETS_ARGS)
def test_check_target(args):
output_shape = args['output_shape']
target = args['target']
nb_samples = args['nb_samples']
bad_target = args['bad_target']

if bad_target:
with pytest.raises((ValueError, AttributeError)):
_check_target(output_shape, target, nb_samples)
else:
_check_target(output_shape, target, nb_samples)


@pytest.mark.parametrize('args', SELECT_ARGS)
def test_select_target(args):
preds = tf.convert_to_tensor(args['preds'])
target = tf.convert_to_tensor(args['target'])
expected = args['expected']

selected = _select_target(preds, target)
assert (expected == selected.numpy()).all()


#####################################################################################################################


Expand Down

0 comments on commit c44fc08

Please sign in to comment.