!26098 Remove explainer code

Merge pull request !26098 from TonyNG/remove_explainer
This commit is contained in:
i-robot 2021-11-12 08:33:11 +00:00 committed by Gitee
commit 48c7b73fd1
36 changed files with 2 additions and 5392 deletions

View File

@ -34,7 +34,6 @@
"mindspore/mindspore/train/serialization.py" "protected-access"
"mindspore/mindspore/train/model.py" "protected-access"
"mindspore/mindspore/log.py" "protected-access"
"mindspore/mindspore/explainer/explanation/_counterfactual/hierarchical_occlusion.py" "unsupported-assignment-operation"
"mindspore/model_zoo/official/cv" "missing-docstring"
"mindspore/model_zoo/official/cv" "c-extension-no-member"
"mindspore/model_zoo/official/nlp/bert_thor/src/bert_model.py" "redefined-outer-name"
@ -117,8 +116,4 @@
"mindspore/tests/st/ops/ascend/test_aicpu_ops/test_strided_slice.py" "redefined-builtin"
"mindspore/tests/st/ops/ascend/test_aicpu_ops/test_strided_slice_grad.py" "redefined-outer-name"
"mindspore/tests/st/pynative/parser/test_parser_construct.py" "bad-super-call"
"mindspore/tests/st/explainer/benchmark/_attribution/test_localization.py" "protected-access"
"mindspore/tests/st/explainer/explanation/_attribution/_backprop/test_gradcam.py" "not-callable"
"mindspore/tests/st/explainer/explanation/_attribution/_backprop/test_gradient.py" "not-callable"
"mindspore/tests/st/explainer/explanation/_attribution/_backprop/test_modified_relu.py" "not-callable"
"mindspore/tests/ut/python/optimizer/test_auto_grad.py" "broad-except"

View File

@ -319,7 +319,6 @@ install(
${CMAKE_SOURCE_DIR}/mindspore/ops
${CMAKE_SOURCE_DIR}/mindspore/communication
${CMAKE_SOURCE_DIR}/mindspore/profiler
${CMAKE_SOURCE_DIR}/mindspore/explainer
${CMAKE_SOURCE_DIR}/mindspore/compression
${CMAKE_SOURCE_DIR}/mindspore/run_check
DESTINATION ${INSTALL_PY_DIR}

View File

@ -205,7 +205,6 @@ install(
${CMAKE_SOURCE_DIR}/mindspore/ops
${CMAKE_SOURCE_DIR}/mindspore/communication
${CMAKE_SOURCE_DIR}/mindspore/profiler
${CMAKE_SOURCE_DIR}/mindspore/explainer
${CMAKE_SOURCE_DIR}/mindspore/compression
${CMAKE_SOURCE_DIR}/mindspore/run_check
DESTINATION ${INSTALL_PY_DIR}

View File

@ -1,6 +0,0 @@
approvers:
- ouwenchang
- wangyue01
- wenkai_dist
- lilongfei15
- lixiaohui33

View File

@ -1,19 +0,0 @@
# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Provides explanation runner high-level APIs."""
from ._image_classification_runner import ImageClassificationRunner
__all__ = ['ImageClassificationRunner']

File diff suppressed because it is too large Load Diff

View File

@ -1,262 +0,0 @@
# Copyright 2020-2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Packaged operations based on MindSpore."""
__all__ = [
'absolute',
'arange',
'argmax',
'argmin',
'argsort',
'assign',
'intersection',
'matmul',
'maximum',
'minimum',
'mean',
'mul',
'sort',
'sqrt',
'squeeze',
'tile',
'reshape',
'zeros',
'zeros_like',
'softmax',
'Tensor',
'summation'
]
from typing import List, Tuple, Union, Callable
import numpy as np
import mindspore
from mindspore import nn
import mindspore.ops.operations as op
_Axis = Union[int, Tuple[int, ...], List[int]]
_Idx = Union[int, mindspore.Tensor, Tuple[int, ...], Tuple[mindspore.Tensor, ...]]
_Number = Union[int, float, np.int, np.float]
_Shape = Union[int, Tuple[int, ...]]
Tensor = mindspore.Tensor
def absolute(inputs: Tensor) -> Tensor:
"""Get the absolute value of a tensor value."""
abs_op = op.Abs()
outputs = abs_op(inputs)
return outputs
def arange(
start: _Number,
end: _Number,
step: _Number = 1,
dtype: mindspore.dtype = None) -> Tensor:
"""Get the arange value of tensor."""
nums = np.arange(start=start, stop=end, step=step, dtype=np.int32)
nums = mindspore.Tensor(nums, dtype=dtype)
return nums
def argmax(inputs: Tensor, axis: int = -1, keep_dims: bool = False) -> Tensor:
"""Returns the indices of the maximum values along an axis."""
inputs_np = inputs.asnumpy()
outputs = np.argmax(inputs_np, axis=axis)
if keep_dims:
outputs = np.expand_dims(outputs, axis=axis)
return mindspore.Tensor(outputs, mindspore.int32)
def argmin(inputs: Tensor, axis: int = -1, keep_dims: bool = False) -> Tensor:
"""Returns the indices of the minimum values along an axis."""
inputs_np = inputs.asnumpy()
outputs = np.argmin(inputs_np, axis=axis)
if keep_dims:
outputs = np.expand_dims(outputs, axis=axis)
return mindspore.Tensor(outputs, mindspore.int32)
def argsort(inputs: Tensor, axis: int = -1, descending: bool = False) -> Tensor:
"""Returns the indices that would sort an array."""
inputs_np = inputs.asnumpy()
factor = -1 if descending else 1
indices_np = np.argsort(factor * inputs_np, axis=axis)
indices = mindspore.Tensor(indices_np, dtype=mindspore.int32)
return indices
def assign(inputs: Tensor, idx: _Idx, value: Tensor) -> Tensor:
"""Assign a tensor value to the given tensor and index."""
inputs_np = inputs.asnumpy()
if isinstance(idx, Tensor):
idx = idx.asnumpy()
value_np = value.asnumpy()
inputs_np[idx] = value_np
outputs = mindspore.Tensor(inputs_np)
return outputs
def intersection(*inputs: Tensor) -> Tensor:
"""Get the intersection value by the given tensor list."""
outputs_np = np.ones_like(inputs[0])
for inp in inputs:
outputs_np &= inp.asnumpy()
outputs = mindspore.Tensor(outputs_np)
return outputs
def matmul(inputs_x: Tensor, inputs_y: Tensor) -> Tensor:
"""Multiplies matrix `inputs_x` and matrix `inputs_y`."""
matmul_op = op.MatMul()
outputs = matmul_op(inputs_x, inputs_y)
return outputs
def maximum(inputs: Tensor, axis: _Axis = (), keep_dims: bool = False) -> Tensor:
"""Reduces a dimension of a tensor by the maximum value in this dimension."""
max_op = op.ReduceMax(keep_dims)
outputs = max_op(inputs, axis)
return outputs
def minimum(inputs: Tensor, axis: _Axis = (), keep_dims: bool = False) -> Tensor:
"""Reduces a dimension of a tensor by the minimum value in the dimension."""
max_op = op.ReduceMin(keep_dims)
outputs = max_op(inputs, axis)
return outputs
def mean(inputs: Tensor, axis: _Axis = (), keep_dims: bool = False) -> Tensor:
"""Reduces a dimension of a tensor by averaging all elements in the dimension."""
mean_op = op.ReduceMean(keep_dims)
outputs = mean_op(inputs, axis)
return outputs
def mul(inputs_x: Tensor, inputs_y: Tensor) -> Tensor:
"""
Multiplies two tensors element-wise.
Inputs of `input_x` and `input_y` comply with the implicit type conversion rules to make the data types consistent.
The inputs must be two tensors or one tensor and one scalar.
When the inputs are two tensors,
dtypes of them cannot be both bool, and the shapes of them could be broadcast.
When the inputs are one tensor and one scalar,
the scalar could only be a constant.
Inputs:
- **input_x** (Union[Tensor, Number, bool]) - The first input is a number or
a bool or a tensor whose data type is number or bool.
- **input_y** (Union[Tensor, Number, bool]) - The second input is a number or
a bool when the first input is a tensor or a tensor whose data type is number or bool.
Outputs:
Tensor, the shape is the same as the one after broadcasting,
and the data type is the one with higher precision or higher digits among the two inputs.
"""
mul_op = op.Mul()
outputs = mul_op(inputs_x, inputs_y)
return outputs
def sort(inputs: Tensor, axis: _Axis = -1, descending: bool = False) -> Tensor:
"""Return a sorted copy of an array."""
inputs_np = inputs.asnumpy()
outputs_np = np.sort(inputs_np, axis=axis)
if descending:
outputs_np = np.flip(outputs_np, axis=axis)
outputs = mindspore.Tensor(outputs_np)
return outputs
def squeeze(inputs: Tensor, axis: _Axis = ()):
"""Returns a tensor with the same type but dimensions of 1 are removed based on `axis`."""
squeeze_op = op.Squeeze(axis)
outputs = squeeze_op(inputs)
return outputs
def tile(inputs: Tensor, shape: Tuple[int, ...]) -> Tensor:
"""Replicates a tensor with given multiples times."""
tile_op = op.Tile()
outputs = tile_op(inputs, shape)
return outputs
def reshape(inputs: Tensor, shape: _Shape) -> Tensor:
"""Reshapes input tensor with the same values based on a given shape tuple."""
if isinstance(shape, int):
shape = (shape,)
return op.Reshape()(inputs, shape)
def zeros(shape: _Shape, dtype: mindspore.dtype = None) -> Tensor:
"""Return a new array of given shape and type, filled with zeros."""
outputs = np.zeros(shape)
return mindspore.Tensor(outputs, dtype=dtype)
def zeros_like(inputs: Tensor, dtype: mindspore.dtype = None) -> Tensor:
"""Return an array of zeros with the same shape and type as a given array."""
inputs_np = inputs.asnumpy()
outputs_np = np.zeros_like(inputs_np)
outputs = mindspore.Tensor(outputs_np, dtype)
return outputs
def random(shape: _Shape, dtype: mindspore.dtype = None) -> Tensor:
"""Return random floats in the half-open interval [0.0, 1.0)."""
outputs_np = np.random.random(shape)
outputs = mindspore.Tensor(outputs_np, dtype)
return outputs
def randint(low: int, high: int, shape: _Shape, dtype: mindspore.dtype = mindspore.int8) -> Tensor:
"""Return random integers from `low` (inclusive) to `high` (exclusive)."""
outputs_np = np.random.randint(low, high, size=shape)
outputs = mindspore.Tensor(outputs_np, dtype=dtype)
return outputs
def softmax(axis: int = -1) -> Callable:
"""Softmax activation function."""
func = nn.Softmax(axis=axis)
return func
def summation(inputs: Tensor, axis: _Axis = (), keep_dims: bool = False) -> Tensor:
"""Reduces a dimension of a tensor by summing all elements in the dimension."""
sum_op = op.ReduceSum(keep_dims)
outputs = sum_op(inputs, axis)
return outputs
def stack(inputs: List[Tensor], axis: int) -> Tensor:
"""Stacks a list of tensors in specified axis."""
stack_op = op.Stack(axis)
outputs = stack_op(inputs)
return outputs
def sqrt(inputs: Tensor) -> Tensor:
"""Returns square root of a tensor element-wise."""
sqrt_op = op.Sqrt()
return sqrt_op(inputs)

View File

@ -1,312 +0,0 @@
# Copyright 2020-2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Utils for MindExplain"""
__all__ = [
'ForwardProbe',
'abs_max',
'calc_auc',
'calc_correlation',
'deprecated_error',
'format_tensor_to_ndarray',
'generate_one_hot',
'rank_pixels',
'resize',
'retrieve_layer_by_name',
'retrieve_layer',
'unify_inputs',
'unify_targets'
]
from typing import Tuple, Union
import numpy as np
from PIL import Image
import mindspore as ms
import mindspore.nn as nn
import mindspore.ops.operations as op
_Array = np.ndarray
_Module = nn.Cell
_Tensor = ms.Tensor
class DeprecatedError(RuntimeError):
def __init__(self):
super().__init__("'mindspore.explainer' is deprecated from version 1.5 and "
"will be removed in a future version, use MindSpore XAI "
"https://gitee.com/mindspore/xai instead.")
def deprecated_error(func_or_cls):
del func_or_cls
raise DeprecatedError()
def abs_max(gradients):
"""
Transform gradients to saliency through abs then take max along channels.
Args:
gradients (_Tensor): Gradients which will be transformed to saliency map.
Returns:
_Tensor, saliency map integrated from gradients.
"""
gradients = op.Abs()(gradients)
saliency = op.ReduceMax(keep_dims=True)(gradients, axis=1)
return saliency
def generate_one_hot(indices, depth):
r"""
Simple wrap of OneHot operation, the on_value an off_value are fixed to 1.0
and 0.0.
"""
on_value = ms.Tensor(1.0, ms.float32)
off_value = ms.Tensor(0.0, ms.float32)
weights = op.OneHot()(indices, depth, on_value, off_value)
return weights
def unify_inputs(inputs) -> tuple:
"""Unify inputs of explainer."""
if isinstance(inputs, tuple):
return inputs
if isinstance(inputs, ms.Tensor):
inputs = (inputs,)
elif isinstance(inputs, np.ndarray):
inputs = (ms.Tensor(inputs),)
else:
raise TypeError(
'inputs must be one of [tuple, ms.Tensor or np.ndarray], '
'but get {}'.format(type(inputs)))
return inputs
def unify_targets(targets) -> ms.Tensor:
"""Unify targets labels of explainer."""
if isinstance(targets, ms.Tensor):
return targets
if isinstance(targets, list):
targets = ms.Tensor(targets, dtype=ms.int32)
if isinstance(targets, int):
targets = ms.Tensor([targets], dtype=ms.int32)
else:
raise TypeError(
'targets must be one of [int, list or ms.Tensor], '
'but get {}'.format(type(targets)))
return targets
def retrieve_layer_by_name(model: _Module, layer_name: str):
"""
Retrieve the layer in the model by the given layer_name.
Args:
model (Cell): Model which contains the target layer.
layer_name (str): Name of target layer.
Returns:
Cell, the target layer.
Raises:
ValueError: If module with given layer_name is not found in the model.
"""
if not isinstance(layer_name, str):
raise TypeError('layer_name should be type of str, but receive {}.'
.format(type(layer_name)))
if not layer_name:
return model
target_layer = None
for name, cell in model.cells_and_names():
if name == layer_name:
target_layer = cell
return target_layer
if target_layer is None:
raise ValueError(
'Cannot match {}, please provide target layer'
'in the given model.'.format(layer_name))
return None
def retrieve_layer(model: _Module, target_layer: Union[str, _Module] = ''):
"""
Retrieve the layer in the model.
'target' can be either a layer name or a Cell object. Given the layer name,
the method will search thourgh the model and return the matched layer. If a
Cell object is provided, it will check whether the given layer exists
in the model. If target layer is not found in the model, ValueError will
be raised.
Args:
model (Cell): Model which contains the target layer.
target_layer (str, Cell): Name of target layer or the target layer instance.
Returns:
Cell, the target layer.
Raises:
ValueError: If module with given layer_name is not found in the model.
"""
if isinstance(target_layer, str):
target_layer = retrieve_layer_by_name(model, target_layer)
return target_layer
if isinstance(target_layer, _Module):
for _, cell in model.cells_and_names():
if target_layer is cell:
return target_layer
raise ValueError(
'Model not contain cell {}, fail to probe.'.format(target_layer)
)
raise TypeError('layer_name must have type of str or ms.nn.Cell,'
'but receive {}'.format(type(target_layer)))
class ForwardProbe:
"""
Probe to capture output of specific layer in a given model.
Args:
target_layer (str, Cell): Name of target layer or the target layer instance.
"""
def __init__(self, target_layer: _Module):
self._target_layer = target_layer
self._original_construct = self._target_layer.construct
self._intermediate_tensor = None
@property
def value(self):
"""Obtain the intermediate tensor."""
return self._intermediate_tensor
def __enter__(self):
self._target_layer.construct = self._new_construct
return self
def __exit__(self, *_):
self._target_layer.construct = self._original_construct
self._intermediate_tensor = None
return False
def _new_construct(self, *inputs):
outputs = self._original_construct(*inputs)
self._intermediate_tensor = outputs
return outputs
def format_tensor_to_ndarray(x: Union[ms.Tensor, np.ndarray]) -> np.ndarray:
"""Unify Tensor and numpy.array to numpy.array."""
if isinstance(x, ms.Tensor):
x = x.asnumpy()
if not isinstance(x, np.ndarray):
raise TypeError('input should be one of [ms.Tensor or np.ndarray],'
' but receive {}'.format(type(x)))
return x
def calc_correlation(x: Union[ms.Tensor, np.ndarray],
y: Union[ms.Tensor, np.ndarray]) -> float:
"""Calculate Pearson correlation coefficient between two vectors."""
x = format_tensor_to_ndarray(x)
y = format_tensor_to_ndarray(y)
if len(x.shape) > 1 or len(y.shape) > 1:
raise ValueError('"calc_correlation" only support 1-dim vectors currently, but get shape {} and {}.'
.format(len(x.shape), len(y.shape)))
if np.all(x == 0) or np.all(y == 0):
return np.float(0)
faithfulness = np.corrcoef(x, y)[0, 1]
return faithfulness
def calc_auc(x: _Array) -> _Array:
"""Calculate the Area under Curve."""
# take mean for multiple patches if the model is fully convolutional model
if len(x.shape) == 4:
x = np.mean(np.mean(x, axis=2), axis=3)
auc = (x.sum() - x[0] - x[-1]) / len(x)
return auc
def rank_pixels(inputs: _Array, descending: bool = True) -> _Array:
"""
Generate rank order for every pixel in an 2D array.
The rank order start from 0 to (num_pixel-1). If descending is True, the
rank order will generate in a descending order, otherwise in ascending
order.
"""
if len(inputs.shape) < 2 or len(inputs.shape) > 3:
raise ValueError('Only support 2D or 3D inputs currently.')
batch_size = inputs.shape[0]
flatten_saliency = inputs.reshape(batch_size, -1)
factor = -1 if descending else 1
sorted_arg = np.argsort(factor * flatten_saliency, axis=1)
flatten_rank = np.zeros_like(sorted_arg)
arange = np.arange(flatten_saliency.shape[1])
for i in range(batch_size):
flatten_rank[i][sorted_arg[i]] = arange
rank_map = flatten_rank.reshape(inputs.shape)
return rank_map
def resize(inputs: _Tensor, size: Tuple[int, int], mode: str) -> _Tensor:
"""
Resize the intermediate layer _attribution to the same size as inputs.
Args:
inputs (Tensor): The input tensor to be resized.
size (tuple[int]): The targeted size resize to.
mode (str): The resize mode. Options: 'nearest_neighbor', 'bilinear'.
Returns:
Tensor, the resized tensor.
Raises:
ValueError: the resize mode is not in ['nearest_neighbor', 'bilinear'].
"""
h, w = size
if mode == 'nearest_neighbor':
resize_nn = op.ResizeNearestNeighbor((h, w))
outputs = resize_nn(inputs)
elif mode == 'bilinear':
inputs_np = inputs.asnumpy()
inputs_np = np.transpose(inputs_np, [0, 2, 3, 1])
array_lst = []
for inp in inputs_np:
array = (np.repeat(inp, 3, axis=2) * 255).astype(np.uint8)
image = Image.fromarray(array)
image = image.resize(size, resample=Image.BILINEAR)
array = np.asarray(image).astype(np.float32) / 255
array_lst.append(array[:, :, 0:1])
resized_np = np.transpose(array_lst, [0, 3, 1, 2])
outputs = ms.Tensor(resized_np, inputs.dtype)
else:
raise ValueError('Unsupported resize mode {}.'.format(mode))
return outputs

View File

@ -1,27 +0,0 @@
# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Predefined XAI metrics."""
from ._attribution.class_sensitivity import ClassSensitivity
from ._attribution.faithfulness import Faithfulness
from ._attribution.localization import Localization
from ._attribution.robustness import Robustness
__all__ = [
"ClassSensitivity",
"Faithfulness",
"Localization",
"Robustness"
]

View File

@ -1,15 +0,0 @@
# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Predefined XAI metrics"""

View File

@ -1,93 +0,0 @@
# Copyright 2020-2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Class Sensitivity."""
import numpy as np
from mindspore.explainer.explanation import RISE
from .metric import LabelAgnosticMetric
from ... import _operators as ops
from ..._utils import calc_correlation, deprecated_error
@deprecated_error
class ClassSensitivity(LabelAgnosticMetric):
"""
Class sensitivity metric used to evaluate attribution-based explanations.
Reasonable atrribution-based explainers are expected to generate distinct saliency maps for different labels,
especially for labels of highest confidence and low confidence. ClassSensitivity evaluates the explainer through
computing the correlation between saliency maps of highest-confidence and lowest-confidence labels. Explainer with
better class sensitivity will receive lower correlation score. To make the evaluation results intuitive, the
returned score will take negative on correlation and normalize.
Supported Platforms:
``Ascend`` ``GPU``
"""
def evaluate(self, explainer, inputs):
"""
Evaluate class sensitivity on a single data sample.
Args:
explainer (Explanation): The explainer to be evaluated, see `mindspore.explainer.explanation`.
inputs (Tensor): A data sample, a 4D tensor of shape :math:`(N, C, H, W)`.
Returns:
numpy.ndarray, 1D array of shape :math:`(N,)`, result of class sensitivity evaluated on `explainer`.
Raises:
TypeError: Be raised for any argument type problem.
Examples:
>>> import numpy as np
>>> import mindspore as ms
>>> from mindspore.explainer.benchmark import ClassSensitivity
>>> from mindspore.explainer.explanation import Gradient
>>> from mindspore import context
>>>
>>> context.set_context(mode=context.PYNATIVE_MODE)
>>> # The detail of LeNet5 is shown in model_zoo.official.cv.lenet.src.lenet.py
>>> net = LeNet5(10, num_channel=3)
>>> # prepare your explainer to be evaluated, e.g., Gradient.
>>> gradient = Gradient(net)
>>> input_x = ms.Tensor(np.random.rand(1, 3, 32, 32), ms.float32)
>>> class_sensitivity = ClassSensitivity()
>>> res = class_sensitivity.evaluate(gradient, input_x)
>>> print(res.shape)
(1,)
"""
self._check_evaluate_param(explainer, inputs)
outputs = explainer.network(inputs)
max_confidence_label = ops.argmax(outputs)
min_confidence_label = ops.argmin(outputs)
if isinstance(explainer, RISE):
labels = ops.stack([max_confidence_label, min_confidence_label], axis=1)
full_saliency = explainer(inputs, labels)
max_confidence_saliency = full_saliency[:, max_confidence_label].asnumpy()
min_confidence_saliency = full_saliency[:, min_confidence_label].asnumpy()
else:
max_confidence_saliency = explainer(inputs, max_confidence_label).asnumpy()
min_confidence_saliency = explainer(inputs, min_confidence_label).asnumpy()
correlations = []
for i in range(inputs.shape[0]):
correlation = calc_correlation(max_confidence_saliency[i].reshape(-1),
min_confidence_saliency[i].reshape(-1))
normalized_correlation = (-correlation + 1) / 2
correlations.append(normalized_correlation)
return np.array(correlations, np.float)

View File

@ -1,468 +0,0 @@
# Copyright 2020-2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Faithfulness."""
from decimal import Decimal
from typing import Callable, Optional, Union
import numpy as np
import mindspore as ms
from mindspore import log, nn
from mindspore.train._utils import check_value_type
from .metric import LabelSensitiveMetric
from ..._utils import calc_auc, deprecated_error, format_tensor_to_ndarray
from ...explanation._attribution import Attribution as _Attribution
from ...explanation._attribution._perturbation.replacement import Constant, GaussianBlur
from ...explanation._attribution._perturbation.ablation import AblationWithSaliency
_Array = np.ndarray
_Explainer = Union[_Attribution, Callable]
_Label = Union[int, ms.Tensor]
_Module = nn.Cell
def _calc_feature_importance(saliency: _Array, masks: _Array) -> _Array:
"""Calculate feature important w.r.t given masks."""
if saliency.shape[1] < masks.shape[2]:
saliency = np.repeat(saliency, repeats=masks.shape[2], axis=1)
batch_size = masks.shape[0]
num_perturbations = masks.shape[1]
saliency = np.repeat(saliency, repeats=num_perturbations, axis=0)
saliency = saliency.reshape([batch_size, num_perturbations, -1])
masks = masks.reshape([batch_size, num_perturbations, -1])
feature_importance = saliency * masks
feature_importance = feature_importance.sum(-1) / masks.sum(-1)
return feature_importance
class _FaithfulnessHelper:
"""Base class for faithfulness calculator."""
_support = [Constant, GaussianBlur]
def __init__(self,
perturb_percent: float,
perturb_mode: str,
perturb_method: str,
is_accumulate: bool,
perturb_pixel_per_step: Optional[int] = None,
num_perturbations: Optional[int] = None,
**kwargs):
self._get_reference = None
for method in self._support:
if perturb_method == method.__name__:
self._get_reference = method(**kwargs)
if self._get_reference is None:
raise ValueError(
'The param "perturb_method" should be one of {}.'.format([x.__name__ for x in self._support]))
self._ablation = AblationWithSaliency(perturb_mode=perturb_mode,
perturb_percent=perturb_percent,
perturb_pixel_per_step=perturb_pixel_per_step,
num_perturbations=num_perturbations,
is_accumulate=is_accumulate)
def calc_faithfulness(self, inputs, model, targets, saliency):
"""Calc faithfulness."""
raise NotImplementedError
class NaiveFaithfulness(_FaithfulnessHelper):
"""
Calculator for naive faithfulness.
Naive faithfulness, the metric replace several pixels on original image by
specific method for each perturbations. The metric predicts on the perturbed
images and record a series of probabilities. Then calculates the
correlation between prob distribution and averaged feature importance.
Higher correlation indicates better faithfulness.
Args:
perturb_percent (float): percentage of pixels to perturb
perturb_method (str): specify the method to replace the pixel.
Current support: ['Constant', 'GaussianBlur']
is_accumulate (bool): whether to accumulate the former perturbations to
the later perturbations.
Default: False.
perturb_pixel_per_step (Optional[int]): number of pixel to perturb
for each perturbation. If perturb_pixel_per_step is None, actual
perturb_pixel_per_step will be calculate by:
num_image_pixel * perturb_percent / num_perturb_steps.
Default: None
num_perturbations (Optional[int]): number of perturbations. If
num_perturbations if None, it will be calculated by:
num_image_pixel * perturb_percent / perturb_pixel_per_step.
Default: None
kwargs: specific perturb_method will require
different arguments. Below lists required args for each method.
'Constant': base_value (int)
'GaussianBlur': sigma (float): 0.7
Raises:
ValueError: Be raised for any argument value problem.
"""
def __init__(self,
perturb_percent: float,
perturb_method: str,
is_accumulate: bool = False,
perturb_pixel_per_step: Optional[int] = None,
num_perturbations: Optional[int] = None,
**kwargs):
super().__init__(perturb_percent=perturb_percent,
perturb_mode='Deletion',
perturb_method=perturb_method,
is_accumulate=is_accumulate,
perturb_pixel_per_step=perturb_pixel_per_step,
num_perturbations=num_perturbations,
**kwargs)
def calc_faithfulness(self,
inputs: _Array,
model: _Module,
targets: _Label,
saliency: _Array) -> np.ndarray:
"""
Calculate naive faithfulness.
Args:
inputs (_Array): sample to calculate faithfulness score
model (_Module): model to explanation
targets (_Label): label to explanation on.
saliency (_Array): Saliency map of given inputs and targets from the
explainer.
Return:
- faithfulness (np.ndarray): faithfulness score
"""
if Decimal(str(saliency.max())) == Decimal(str(saliency.min())):
log.warning("The saliency map is uniform everywhere. The correlation will be set to zero.")
correlation = 0
return np.array([correlation], np.float)
batch_size = inputs.shape[0]
reference = self._get_reference(inputs)
masks = self._ablation.generate_mask(saliency, inputs.shape[1])
perturbations = self._ablation(inputs, reference, masks)
feature_importance = _calc_feature_importance(saliency, masks)
perturbations = perturbations.reshape(-1, *perturbations.shape[2:])
perturbations = ms.Tensor(perturbations, dtype=ms.float32)
predictions = model(perturbations)[:, targets].asnumpy()
predictions = predictions.reshape(*feature_importance.shape)
if Decimal(str(predictions.max())) == Decimal(str(predictions.min())):
log.warning("The perturbations do not affect the predictions. The correlation will be set to zero.")
correlation = 0
return np.array([correlation], np.float)
faithfulness = -np.corrcoef(feature_importance, predictions)
faithfulness = np.diag(faithfulness[:batch_size, batch_size:])
return faithfulness
class DeletionAUC(_FaithfulnessHelper):
""" Calculator for deletion AUC.
For Deletion AUC, the metric accumulative replace pixels on origin
images through specific 'perturb_method', predict on the perturbed images
and record series of probabilities. The metric then calculates the AUC of
the probability variation curve during perturbations. Faithfulness is define
as (1 - deletion_AUC). Higher score indicates better faithfulness of
explanation.
Args:
perturb_percent (float): percentage of pixels to perturb
perturb_method (str): specify the method to replace the pixel.
Current support: ['Constant', 'GaussianBlur']
perturb_pixel_per_step (Optional[int]): number of pixel to perturb
for each perturbation. If perturb_pixel_per_step is None, actual
perturb_pixel_per_step will be calculate by:
num_image_pixel * perturb_percent / num_perturb_steps.
Default: None
num_perturbations (Optional[int]): number of perturbations. If
num_perturbations if None, it will be calculated by:
num_image_pixel * perterb_percent / perturb_pixel_per_step.
Default: None
kwargs: specific perturb_method will require
different arguments. Below lists required args for each method.
'Constant': base_value (int)
'GaussianBlur': sigma (float): 0.7
Raises:
ValueError: Be raised for any argument value problem.
"""
def __init__(self,
perturb_percent: float,
perturb_method: str,
perturb_pixel_per_step: Optional[int] = None,
num_perturbations: Optional[int] = None,
**kwargs):
super().__init__(perturb_percent=perturb_percent,
perturb_mode='Deletion',
perturb_method=perturb_method,
perturb_pixel_per_step=perturb_pixel_per_step,
num_perturbations=num_perturbations,
is_accumulate=True,
**kwargs)
def calc_faithfulness(self,
inputs: _Array,
model: _Module,
targets: _Label,
saliency: _Array) -> _Array:
"""
Calculate faithfulness through deletion AUC.
Args:
inputs (_Array): sample to calculate faithfulness score
model (_Module): model to explanation
targets (_Label): label to explanation on.
saliency (_Array): Saliency map of given inputs and targets from the
explainer.
Return:
- faithfulness (float): faithfulness score
"""
reference = self._get_reference(inputs)
masks = self._ablation.generate_mask(saliency, inputs.shape[1])
perturbations = self._ablation(inputs, reference, masks)
perturbations = perturbations.reshape(-1, *perturbations.shape[2:])
perturbations = ms.Tensor(perturbations, dtype=ms.float32)
predictions = model(perturbations).asnumpy()[:, targets]
predictions = predictions.reshape((inputs.shape[0], -1))
input_tensor = ms.Tensor(inputs, ms.float32)
original_output = model(input_tensor).asnumpy()[:, targets]
auc = calc_auc(original_output.squeeze() - predictions.squeeze())
return np.array([1 - auc], np.float)
class InsertionAUC(_FaithfulnessHelper):
""" Calculator for insertion AUC.
For Insertion AUC, the metric accumulative replace pixels of reference
image by pixels from origin image, like inserting pixel from origin image to
reference. The reference if generated through specific 'perturb_method'.
The metric predicts on the perturbed images and records series of
probabilities. The metric then calculates the AUC of the probability
variation curve during perturbations. Faithfulness is define as (1 -
deletion_AUC). Higher score indicates better faithfulness of explanation.
Args:
perturb_percent (float): percentage of pixels to perturb
perturb_method (str): specify the method to replace the pixel.
Current support: ['Constant', 'GaussianBlur']
perturb_pixel_per_step (Optional[int]): number of pixel to perturb
for each perturbation. If perturb_pixel_per_step is None, actual
perturb_pixel_per_step will be calculate by:
num_image_pixel * perturb_percent / num_perturb_steps.
Default: None
num_perturbations (Optional[int]): number of perturbations. If
num_perturbations if None, it will be calculated by:
num_image_pixel * perterb_percent / perturb_pixel_per_step.
Default: None
kwargs: specific perturb_method will require
different arguments. Below lists required args for each method.
'Constant': base_value (int)
'GaussianBlur': sigma (float): 0.7
Raises:
ValueError: Be raised for any argument value problem.
"""
def __init__(self,
perturb_percent: float,
perturb_method: str,
perturb_pixel_per_step: Optional[int] = None,
num_perturbations: Optional[int] = None,
**kwargs):
super().__init__(perturb_percent=perturb_percent,
perturb_mode='Insertion',
perturb_method=perturb_method,
perturb_pixel_per_step=perturb_pixel_per_step,
num_perturbations=num_perturbations,
is_accumulate=True,
**kwargs)
def calc_faithfulness(self,
inputs: _Array,
model: _Module,
targets: _Label,
saliency: _Array) -> _Array:
"""
Calculate faithfulness through insertion AUC.
Args:
inputs (_Array): sample to calculate faithfulness score
model (_Module): model to explanation
targets (_Label): label to explanation on.
saliency (_Array): Saliency map of given inputs and targets from the
explainer.
Return:
- faithfulness (float): faithfulness score
"""
reference = self._get_reference(inputs)
masks = self._ablation.generate_mask(saliency, inputs.shape[1])
perturbations = self._ablation(inputs, reference, masks)
perturbations = perturbations.reshape(-1, *perturbations.shape[2:])
perturbations = ms.Tensor(perturbations, dtype=ms.float32)
predictions = model(perturbations).asnumpy()[:, targets]
predictions = predictions.reshape((inputs.shape[0], -1))
base_tensor = ms.Tensor(reference, ms.float32)
base_outputs = model(base_tensor).asnumpy()[:, targets]
auc = calc_auc(predictions.squeeze() - base_outputs.squeeze())
return np.array([auc], np.float)
@deprecated_error
class Faithfulness(LabelSensitiveMetric):
"""
Provides evaluation on faithfulness on XAI explanations.
Three specific metrics to obtain quantified results are supported: "NaiveFaithfulness", "DeletionAUC", and
"InsertionAUC".
For metric "NaiveFaithfulness", a series of perturbed images are created by modifying pixels
on original image. Then the perturbed images will be fed to the model and a series of output probability drops can
be obtained. The faithfulness is then quantified as the correlation between the propability drops and the saliency
map values on the same pixels (we normalize the correlation further to make them in range of [0, 1]).
For metric "DeletionAUC", a series of perturbed images are created by accumulatively modifying pixels of the
original image to a base value (e.g. a constant). The perturbation starts from pixels with high saliency values
to pixels with low saliency values. Feeding the perturbed images into the model in order, an output probability
drop curve can be obtained. "DeletionAUC" is then obtained as the area under this probability drop curve.
For metric "InsertionAUC", a series of perturbed images are created by accumulatively inserting pixels of the
original image to a reference image (e.g. a black image). The insertion starts from pixels with high saliency
values to pixels with low saliency values. Feeding the perturbed images into the model in order, an output
probability increase curve can be obtained. "InsertionAUC" is then obtained as the area under this curve.
For all the three metrics, higher value indicates better faithfulness.
Args:
num_labels (int): Number of labels.
activation_fn (Cell): The activation layer that transforms logits to prediction probabilities. For
single label classification tasks, `nn.Softmax` is usually applied. As for multi-label classification
tasks, `nn.Sigmoid` is usually be applied. Users can also pass their own customized `activation_fn` as long
as when combining this function with network, the final output is the probability of the input.
metric (str, optional): The specifi metric to quantify faithfulness.
Options: "DeletionAUC", "InsertionAUC", "NaiveFaithfulness".
Default: 'NaiveFaithfulness'.
Raises:
TypeError: Be raised for any argument type problem.
Supported Platforms:
``Ascend`` ``GPU``
"""
_methods = [NaiveFaithfulness, DeletionAUC, InsertionAUC]
def __init__(self, num_labels, activation_fn, metric="NaiveFaithfulness"):
super(Faithfulness, self).__init__(num_labels)
perturb_percent = 0.5 # ratio of pixels to be perturbed, future argument
perturb_method = "Constant" # perturbation method, all the perturbed pixels will be set to constant
base_value = 0.0 # the pixel value set for the perturbed pixels
check_value_type("activation_fn", activation_fn, nn.Cell)
self._activation_fn = activation_fn
self._verify_metrics(metric)
for method in self._methods:
if metric == method.__name__:
self._faithfulness_helper = method(
perturb_percent=perturb_percent,
perturb_method=perturb_method,
base_value=base_value
)
def evaluate(self, explainer, inputs, targets, saliency=None):
"""
Evaluate faithfulness on a single data sample.
Note:
Currently only single sample (:math:`N=1`) at each call is supported.
Args:
explainer (Explanation): The explainer to be evaluated, see `mindspore.explainer.explanation`.
inputs (Tensor): A data sample, a 4D tensor of shape :math:`(N, C, H, W)`.
targets (Tensor, int): The label of interest. It should be a 1D or 0D tensor, or an integer.
If `targets` is a 1D tensor, its length should be the same as `inputs`.
saliency (Tensor, optional): The saliency map to be evaluated, a 4D tensor of shape :math:`(N, 1, H, W)`.
If it is None, the parsed `explainer` will generate the saliency map with `inputs` and `targets` and
continue the evaluation. Default: None.
Returns:
numpy.ndarray, 1D array of shape :math:`(N,)`, result of faithfulness evaluated on `explainer`.
Examples:
>>> import numpy as np
>>> import mindspore as ms
>>> from mindspore import nn
>>> from mindspore.explainer.benchmark import Faithfulness
>>> from mindspore.explainer.explanation import Gradient
>>> from mindspore import context
>>>
>>> context.set_context(mode=context.PYNATIVE_MODE)
>>> # init a `Faithfulness` object
>>> num_labels = 10
>>> metric = "InsertionAUC"
>>> activation_fn = nn.Softmax()
>>> faithfulness = Faithfulness(num_labels, activation_fn, metric)
>>> # The detail of LeNet5 is shown in model_zoo.official.cv.lenet.src.lenet.py
>>> net = LeNet5(10, num_channel=3)
>>> gradient = Gradient(net)
>>> inputs = ms.Tensor(np.random.rand(1, 3, 32, 32), ms.float32)
>>> targets = 5
>>> # usage 1: input the explainer and the data to be explained,
>>> # faithfulness is a Faithfulness instance
>>> res = faithfulness.evaluate(gradient, inputs, targets)
>>> print(res.shape)
(1,)
>>> # usage 2: input the generated saliency map
>>> saliency = gradient(inputs, targets)
>>> res = faithfulness.evaluate(gradient, inputs, targets, saliency)
>>> print(res.shape)
(1,)
"""
self._check_evaluate_param(explainer, inputs, targets, saliency)
if saliency is None:
saliency = explainer(inputs, targets)
inputs = format_tensor_to_ndarray(inputs)
saliency = format_tensor_to_ndarray(saliency)
full_network = nn.SequentialCell([explainer.network, self._activation_fn])
faithfulness = self._faithfulness_helper.calc_faithfulness(inputs=inputs, model=full_network,
targets=targets, saliency=saliency)
return (1 + faithfulness) / 2
def _verify_metrics(self, metric: str):
supports = [x.__name__ for x in self._methods]
if metric not in supports:
raise ValueError("Metric should be one of {}.".format(supports))

View File

@ -1,178 +0,0 @@
# Copyright 2020-2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Localization metrics."""
import numpy as np
from mindspore.train._utils import check_value_type
from .metric import LabelSensitiveMetric
from ..._operators import maximum, reshape, Tensor
from ..._utils import deprecated_error, format_tensor_to_ndarray
def _get_max_position(saliency):
"""Get the position of the max pixel of the saliency map."""
saliency = saliency.asnumpy()
w = saliency.shape[3]
saliency = np.reshape(saliency, (len(saliency), -1))
max_arg = np.argmax(saliency, axis=1)
return max_arg // w, max_arg - (max_arg // w) * w
def _mask_out_saliency(saliency, threshold):
"""Keep the saliency map with value greater than threshold."""
max_value = maximum(saliency)
mask_out = saliency > (reshape(max_value, (len(saliency), -1, 1, 1)) * threshold)
return mask_out
@deprecated_error
class Localization(LabelSensitiveMetric):
r"""
Provides evaluation on the localization capability of XAI methods.
Three specific metrics to obtain quantified results are supported: "PointingGame", and "IoSR"
(Intersection over Salient Region).
For metric "PointingGame", the localization capability is calculated as the ratio of data in which the max position
of their saliency maps lies within the bounding boxes. Specifically, for a single datum, given the saliency map and
its bounding box, if the max point of its saliency map lies within the bounding box, the evaluation result is 1
otherwise 0.
For metric "IoSR" (Intersection over Salient Region), the localization capability is calculated as the intersection
of the bounding box and the salient region over the area of the salient region. The salient region is defined as
the region whose value exceeds :math:`\theta * \max{saliency}`.
Args:
num_labels (int): Number of classes in the dataset.
metric (str, optional): Specific metric to calculate localization capability.
Options: "PointingGame", "IoSR". Default: "PointingGame".
Raises:
TypeError: Be raised for any argument type problem.
Supported Platforms:
``Ascend`` ``GPU``
"""
def __init__(self,
num_labels,
metric="PointingGame"
):
super(Localization, self).__init__(num_labels)
self._verify_metrics(metric)
self._metric = metric
# Arg for specific metric, for "PointingGame" it should be an integer indicating the tolerance
# of "PointingGame", while for "IoSR" it should be a float number
# indicating the threshold to choose salient region. Default: 25.
if self._metric == "PointingGame":
self._metric_arg = 15
else:
self._metric_arg = 0.5
@staticmethod
def _verify_metrics(metric):
"""Verify the user defined metric."""
supports = ["PointingGame", "IoSR"]
if metric not in supports:
raise ValueError("Metric should be one of {}".format(supports))
def evaluate(self, explainer, inputs, targets, saliency=None, mask=None):
"""
Evaluate localization on a single data sample.
Note:
Currently only single sample (:math:`N=1`) at each call is supported.
Args:
explainer (Explanation): The explainer to be evaluated, see `mindspore.explainer.explanation`.
inputs (Tensor): A data sample, a 4D tensor of shape :math:`(N, C, H, W)`.
targets (Tensor, int): The label of interest. It should be a 1D or 0D tensor, or an integer.
If `targets` is a 1D tensor, its length should be the same as `inputs`.
saliency (Tensor, optional): The saliency map to be evaluated, a 4D tensor of shape :math:`(N, 1, H, W)`.
If it is None, the parsed `explainer` will generate the saliency map with `inputs` and `targets` and
continue the evaluation. Default: None.
mask (Tensor, numpy.ndarray): Ground truth bounding box/masks for the inputs w.r.t targets, a 4D tensor
or numpy.ndarray of shape :math:`(N, 1, H, W)`.
Returns:
numpy.ndarray, 1D array of shape :math:`(N,)`, result of localization evaluated on `explainer`.
Raises:
ValueError: Be raised for any argument value problem.
Examples:
>>> import numpy as np
>>> import mindspore as ms
>>> from mindspore.explainer.explanation import Gradient
>>> from mindspore.explainer.benchmark import Localization
>>> from mindspore import context
>>>
>>> context.set_context(mode=context.PYNATIVE_MODE)
>>> num_labels = 10
>>> localization = Localization(num_labels, "PointingGame")
>>>
>>> # The detail of LeNet5 is shown in model_zoo.official.cv.lenet.src.lenet.py
>>> net = LeNet5(10, num_channel=3)
>>> gradient = Gradient(net)
>>> inputs = ms.Tensor(np.random.rand(1, 3, 32, 32), ms.float32)
>>> masks = np.zeros([1, 1, 32, 32])
>>> masks[:, :, 10: 20, 10: 20] = 1
>>> targets = 5
>>> # usage 1: input the explainer and the data to be explained,
>>> # localization is a Localization instance
>>> res = localization.evaluate(gradient, inputs, targets, mask=masks)
>>> print(res.shape)
(1,)
>>> # usage 2: input the generated saliency map
>>> saliency = gradient(inputs, targets)
>>> res = localization.evaluate(gradient, inputs, targets, saliency, mask=masks)
>>> print(res.shape)
(1,)
"""
self._check_evaluate_param_with_mask(explainer, inputs, targets, saliency, mask)
mask_np = format_tensor_to_ndarray(mask)[0]
if saliency is None:
saliency = explainer(inputs, targets)
if self._metric == "PointingGame":
point = _get_max_position(saliency)
x, y = np.meshgrid(
(np.arange(mask_np.shape[1]) - point[0]) ** 2,
(np.arange(mask_np.shape[2]) - point[1]) ** 2)
max_region = (x + y) < self._metric_arg ** 2
# if max_region has overlap with mask_np return 1 otherwise 0.
result = 1 if (mask_np.astype(bool) & max_region).any() else 0
elif self._metric == "IoSR":
mask_out_np = format_tensor_to_ndarray(_mask_out_saliency(saliency, self._metric_arg))
overlap = np.sum(mask_np.astype(bool) & mask_out_np.astype(bool))
saliency_area = np.sum(mask_out_np)
result = overlap / saliency_area.clip(min=1e-10)
return np.array([result], np.float)
def _check_evaluate_param_with_mask(self, explainer, inputs, targets, saliency, mask):
self._check_evaluate_param(explainer, inputs, targets, saliency)
if len(inputs.shape) != 4:
raise ValueError('Argument mask must be 4D Tensor')
if mask is None:
raise ValueError('To compute localization, mask must be provided.')
check_value_type('mask', mask, (Tensor, np.ndarray))
if len(mask.shape) != 4 or len(mask) != len(inputs):
raise ValueError("The input mask must be 4-dimensional (1, 1, h, w) with same length of inputs.")

View File

@ -1,222 +0,0 @@
# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Base class for XAI metrics."""
import copy
import math
from typing import Callable
import numpy as np
import mindspore as ms
from mindspore import log as logger
from mindspore.train._utils import check_value_type
from ..._operators import Tensor
from ..._utils import format_tensor_to_ndarray
from ...explanation._attribution.attribution import Attribution
_Explainer = Attribution
def verify_argument(inputs, arg_name):
"""Verify the validity of the parsed arguments."""
check_value_type(arg_name, inputs, Tensor)
if len(inputs.shape) != 4:
raise ValueError('Argument {} must be a 4D Tensor.'.format(arg_name))
if len(inputs) > 1:
raise ValueError('Support single data evaluation only, but got {}.'.format(len(inputs)))
def verify_targets(targets, num_labels):
"""Verify the validity of the parsed targets."""
check_value_type('targets', targets, (int, Tensor))
if isinstance(targets, Tensor):
if len(targets.shape) > 1 or (len(targets.shape) == 1 and len(targets) != 1):
raise ValueError('Argument targets must be a 1D or 0D Tensor. If it is a 1D Tensor, '
'it should have the length = 1 as we only support single evaluation now.')
targets = int(targets.asnumpy()[0]) if len(targets.shape) == 1 else int(targets.asnumpy())
if targets > num_labels - 1 or targets < 0:
raise ValueError('Parsed targets exceed the label range.')
class AttributionMetric:
"""Super class of XAI metric class used in classification scenarios."""
def __init__(self):
self._explainer = None
evaluate: Callable
"""
This method evaluates the explainer on the given attribution and returns the evaluation results.
Derived class should implement this method according to specific algorithms of the metric.
"""
def _record_explainer(self, explainer: _Explainer):
"""Record the explainer in current evaluation."""
if self._explainer is None:
self._explainer = explainer
elif self._explainer is not explainer:
logger.info('Provided explainer is not the same as previously evaluated one. Please reset the evaluated '
'results. Previous explainer: %s, current explainer: %s', self._explainer, explainer)
self._explainer = explainer
class LabelAgnosticMetric(AttributionMetric):
"""Super class add functions for label-agnostic metric."""
def __init__(self):
super().__init__()
self._global_results = []
@property
def performance(self) -> float:
"""
Return the average evaluation result.
Return:
float, averaged result. If no result is aggregate in the global_results, 0.0 will be returned.
"""
result_sum, count = 0, 0
for res in self._global_results:
if math.isfinite(res):
result_sum += res
count += 1
return 0. if count == 0 else result_sum / count
def aggregate(self, result):
"""Aggregate single evaluation result to global results."""
if isinstance(result, float):
self._global_results.append(result)
elif isinstance(result, (ms.Tensor, np.ndarray)):
result = format_tensor_to_ndarray(result)
self._global_results.extend([float(res) for res in result.reshape(-1)])
else:
raise TypeError('result should have type of float, ms.Tensor or np.ndarray, but receive %s' % type(result))
def get_results(self):
"""Return the global results."""
return self._global_results.copy()
def reset(self):
"""Reset global results."""
self._global_results.clear()
def _check_evaluate_param(self, explainer, inputs):
"""Check the evaluate parameters."""
check_value_type('explainer', explainer, Attribution)
self._record_explainer(explainer)
verify_argument(inputs, 'inputs')
class LabelSensitiveMetric(AttributionMetric):
"""Super class add functions for label-sensitive metrics."""
def __init__(self, num_labels: int):
super().__init__()
LabelSensitiveMetric._verify_params(num_labels)
self._num_labels = num_labels
self._global_results = {i: [] for i in range(num_labels)}
@property
def num_labels(self):
"""Number of labels used in evaluation."""
return self._num_labels
@staticmethod
def _verify_params(num_labels):
"""Checks whether num_labels is valid."""
check_value_type("num_labels", num_labels, int)
if num_labels < 1:
raise ValueError("Argument num_labels must be parsed with an integer > 0.")
def aggregate(self, result, targets):
"""Aggregates single result to global_results."""
if isinstance(result, float):
if isinstance(targets, int):
self._global_results[targets].append(result)
else:
target_np = format_tensor_to_ndarray(targets)
if len(target_np) > 1:
raise ValueError("One result can not be aggreated to multiple targets.")
elif isinstance(result, (ms.Tensor, np.ndarray)):
result_np = format_tensor_to_ndarray(result).reshape(-1)
if isinstance(targets, int):
for res in result_np:
self._global_results[targets].append(float(res))
else:
target_np = format_tensor_to_ndarray(targets).reshape(-1)
if len(target_np) != len(result_np):
raise ValueError("Length of result does not match with length of targets.")
for tar, res in zip(target_np, result_np):
self._global_results[int(tar)].append(float(res))
else:
raise TypeError('Result should have type of float, ms.Tensor or np.ndarray, but receive %s' % type(result))
def reset(self):
"""Resets global_result."""
self._global_results = {i: [] for i in range(self._num_labels)}
@property
def class_performances(self):
"""
Get the class performances by global result.
Returns:
(:class:`list`): a list of performances where each value is the average score of specific class.
"""
results_on_labels = []
for label_id in range(self._num_labels):
sum_of_label, count_of_label = 0, 0
for res in self._global_results[label_id]:
if math.isfinite(res):
sum_of_label += res
count_of_label += 1
results_on_labels.append(0. if count_of_label == 0 else sum_of_label / count_of_label)
return results_on_labels
@property
def performance(self):
"""
Get the performance by global result.
Returns:
(:class:`float`): mean performance.
"""
result_sum, count = 0, 0
for label_id in range(self._num_labels):
for res in self._global_results[label_id]:
if math.isfinite(res):
result_sum += res
count += 1
return 0. if count == 0 else result_sum / count
def get_results(self):
"""Global result of the metric can be return"""
return copy.deepcopy(self._global_results)
def _check_evaluate_param(self, explainer, inputs, targets, saliency):
"""Check the evaluate parameters."""
check_value_type('explainer', explainer, Attribution)
self._record_explainer(explainer)
verify_argument(inputs, 'inputs')
output = explainer.network(inputs)
check_value_type("output of explainer model", output, Tensor)
output_dim = explainer.network(inputs).shape[1]
if output_dim != self._num_labels:
raise ValueError("The output dimension of of black-box model in explainer does not match the dimension "
"of num_labels set in the __init__, please check explainer and num_labels again.")
verify_targets(targets, self._num_labels)
check_value_type('saliency', saliency, (Tensor, type(None)))

View File

@ -1,153 +0,0 @@
# Copyright 2020-2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Robustness."""
import numpy as np
import mindspore as ms
import mindspore.nn as nn
from mindspore.train._utils import check_value_type
from mindspore import log
from .metric import LabelSensitiveMetric
from ...explanation._attribution._perturbation.replacement import RandomPerturb
from ..._utils import deprecated_error
@deprecated_error
class Robustness(LabelSensitiveMetric):
"""
Robustness perturbs the inputs by adding random noise and choose the maximum sensitivity as evaluation score from
the perturbations.
Args:
num_labels (int): Number of classes in the dataset.
activation_fn (Cell): The activation layer that transforms logits to prediction probabilities. For
single label classification tasks, `nn.Softmax` is usually applied. As for multi-label classification
tasks, `nn.Sigmoid` is usually be applied. Users can also pass their own customized `activation_fn` as long
as when combining this function with network, the final output is the probability of the input.
Raises:
TypeError: Be raised for any argument type problem.
Supported Platforms:
``Ascend`` ``GPU``
"""
def __init__(self, num_labels, activation_fn):
super().__init__(num_labels)
check_value_type("activation_fn", activation_fn, nn.Cell)
self._perturb = RandomPerturb()
self._num_perturbations = 10 # number of perturbations used in evaluation
self._threshold = 0.1 # threshold to generate perturbation
self._activation_fn = activation_fn
def evaluate(self, explainer, inputs, targets, saliency=None):
"""
Evaluate robustness on single sample.
Note:
Currently only single sample (:math:`N=1`) at each call is supported.
Args:
explainer (Explanation): The explainer to be evaluated, see `mindspore.explainer.explanation`.
inputs (Tensor): A data sample, a 4D tensor of shape :math:`(N, C, H, W)`.
targets (Tensor, int): The label of interest. It should be a 1D or 0D tensor, or an integer.
If `targets` is a 1D tensor, its length should be the same as `inputs`.
saliency (Tensor, optional): The saliency map to be evaluated, a 4D tensor of shape :math:`(N, 1, H, W)`.
If it is None, the parsed `explainer` will generate the saliency map with `inputs` and `targets` and
continue the evaluation. Default: None.
Returns:
numpy.ndarray, 1D array of shape :math:`(N,)`, result of localization evaluated on `explainer`.
Raises:
ValueError: If batch_size is larger than 1.
Examples:
>>> import numpy as np
>>> import mindspore as ms
>>> from mindspore import nn
>>> from mindspore.explainer.explanation import Gradient
>>> from mindspore.explainer.benchmark import Robustness
>>> from mindspore import context
>>>
>>> context.set_context(mode=context.PYNATIVE_MODE)
>>> # Initialize a Robustness benchmarker passing num_labels of the dataset.
>>> num_labels = 10
>>> activation_fn = nn.Softmax()
>>> robustness = Robustness(num_labels, activation_fn)
>>>
>>> # The detail of LeNet5 is shown in model_zoo.official.cv.lenet.src.lenet.py
>>> net = LeNet5(10, num_channel=3)
>>> # prepare your explainer to be evaluated, e.g., Gradient.
>>> gradient = Gradient(net)
>>> input_x = ms.Tensor(np.random.rand(1, 3, 32, 32), ms.float32)
>>> target_label = ms.Tensor([0], ms.int32)
>>> # robustness is a Robustness instance
>>> res = robustness.evaluate(gradient, input_x, target_label)
>>> print(res.shape)
(1,)
"""
self._check_evaluate_param(explainer, inputs, targets, saliency)
if inputs.shape[0] > 1:
raise ValueError('Robustness only support a sample each time, but receive {}'.format(inputs.shape[0]))
if isinstance(targets, int):
targets = ms.Tensor([targets], ms.int32)
if saliency is None:
saliency = explainer(inputs, targets)
saliency = saliency.asnumpy()
norm = np.sqrt(np.sum(np.square(saliency), axis=tuple(range(1, len(saliency.shape)))))
if (norm == 0).any():
log.warning('Get saliency norm equals 0, robustness return NaN for zero-norm saliency currently.')
norm[norm == 0] = np.nan
full_network = nn.SequentialCell([explainer.network, self._activation_fn])
original_outputs = full_network(inputs).asnumpy()
sensitivities = []
inputs = inputs.asnumpy()
for _ in range(self._num_perturbations):
perturbations = []
for j, sample in enumerate(inputs):
perturbation_on_single_sample = self._perturb_with_threshold(full_network,
np.expand_dims(sample, axis=0),
original_outputs[j])
perturbations.append(perturbation_on_single_sample)
perturbations = np.vstack(perturbations)
perturbations = explainer(ms.Tensor(perturbations, ms.float32), targets).asnumpy()
sensitivity = np.sqrt(np.sum((perturbations - saliency) ** 2,
axis=tuple(range(1, len(saliency.shape)))))
sensitivities.append(sensitivity)
sensitivities = np.stack(sensitivities, axis=-1)
sensitivity = np.max(sensitivities, axis=1) / norm
return 1 / np.exp(sensitivity)
def _perturb_with_threshold(self, network: nn.Cell, sample: np.ndarray, original_output: np.ndarray) -> np.ndarray:
"""
Generate the perturbation until the L2-distance between original_output and perturbation_output is lower than
the given self._threshold or until the attempt reaches the max_attempt_time.
"""
# the maximum time attempt to get a perturbation with perturb_error low than self._threshold
max_attempt_time = 3
perturbation = None
for _ in range(max_attempt_time):
perturbation = self._perturb(sample)
perturbation_output = self._activation_fn(network(ms.Tensor(sample, ms.float32))).asnumpy()
perturb_error = np.linalg.norm(original_output - perturbation_output)
if perturb_error <= self._threshold:
return perturbation
return perturbation

View File

@ -1,30 +0,0 @@
# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Predefined Attribution explainers."""
from ._attribution._backprop.gradient import Gradient
from ._attribution._backprop.gradcam import GradCAM
from ._attribution._backprop.modified_relu import Deconvolution, GuidedBackprop
from ._attribution._perturbation.occlusion import Occlusion
from ._attribution._perturbation.rise import RISE
__all__ = [
'Gradient',
'Deconvolution',
'GuidedBackprop',
'GradCAM',
'Occlusion',
'RISE',
]

View File

@ -1,21 +0,0 @@
# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Predefined Attribution explainers."""
from .attribution import Attribution
__all__ = [
'Attribution'
]

View File

@ -1,15 +0,0 @@
# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Backprop-base _attribution explainer."""

View File

@ -1,69 +0,0 @@
# Copyright 2020-2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Providing utility functions."""
from mindspore.nn import Cell
from mindspore.ops.composite import GradOperation
from mindspore.explainer._utils import unify_inputs, unify_targets, generate_one_hot
def get_bp_weights(model, inputs, targets=None, weights=None):
r"""
Compute the gradient of output w.r.t input.
Args:
model (Cell): Differentiable black-box model.
inputs (Tensor): Input to calculate gradient and explanation.
targets (int, optional): Target label id specifying which category to compute gradient. Default: None.
weights (Tensor, optional): Custom weights for computing gradients. The shape of weights should match the model
outputs. If None is provided, an one-hot weights with one in targets positions will be used instead.
Default: None.
Returns:
Tensor, signal to be back-propagated to the input.
"""
inputs = unify_inputs(inputs)
if targets is None and weights is None:
raise ValueError('Must provide one of targets or weights')
if weights is None:
targets = unify_targets(targets)
output = model(*inputs)
num_categories = output.shape[-1]
weights = generate_one_hot(targets, num_categories)
return weights
class GradNet(Cell):
"""
Network for gradient calculation.
Args:
network (Cell): The network to generate backpropagated gradients.
"""
def __init__(self, network):
super(GradNet, self).__init__()
self.network = network
self.grad = GradOperation(get_all=True, sens_param=True)(network)
def construct(self, *input_data):
"""
Get backpropgated gradients.
Returns:
Tensor, output gradients.
"""
gout = self.grad(*input_data)[0]
return gout

View File

@ -1,160 +0,0 @@
# Copyright 2020-2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""GradCAM."""
from mindspore.ops import operations as op
from mindspore.explainer._utils import deprecated_error, ForwardProbe, retrieve_layer, unify_inputs, unify_targets
from .backprop_utils import get_bp_weights, GradNet
from .intermediate_layer import IntermediateLayerAttribution
def _gradcam_aggregation(attributions):
"""
Aggregate the gradient and activation to get the final _attribution.
Args:
attributions (Tensor): the _attribution with channel dimension.
Returns:
Tensor: the _attribution with channel dimension aggregated.
"""
sum_ = op.ReduceSum(keep_dims=True)
relu_ = op.ReLU()
attributions = relu_(sum_(attributions, 1))
return attributions
@deprecated_error
class GradCAM(IntermediateLayerAttribution):
r"""
Provides GradCAM explanation method.
`GradCAM` generates saliency map at intermediate layer. The attribution is obtained as:
.. math::
\alpha_k^c = \frac{1}{Z} \sum_i \sum_j \frac{\partial{y^c}}{\partial{A_{i,j}^k}}
attribution = ReLU(\sum_k \alpha_k^c A^k)
For more details, please refer to the original paper: `GradCAM <https://openaccess.thecvf.com/content_ICCV_2017/
papers/Selvaraju_Grad-CAM_Visual_Explanations_ICCV_2017_paper.pdf>`_.
Note:
The parsed `network` will be set to eval mode through `network.set_grad(False)` and `network.set_train(False)`.
If you want to train the `network` afterwards, please reset it back to training mode through the opposite
operations.
Args:
network (Cell): The black-box model to be explained.
layer (str, optional): The layer name to generate the explanation, usually chosen as the last convolutional
layer for better practice. If it is '', the explanation will be generated at the input layer.
Default: ''.
Inputs:
- **inputs** (Tensor) - The input data to be explained, a 4D tensor of shape :math:`(N, C, H, W)`.
- **targets** (Tensor, int) - The label of interest. It should be a 1D or 0D tensor, or an integer.
If it is a 1D tensor, its length should be the same as `inputs`.
Outputs:
Tensor, a 4D tensor of shape :math:`(N, 1, H, W)`, saliency maps.
Raises:
TypeError: Be raised for any argument or input type problem.
ValueError: Be raised for any input value problem.
Supported Platforms:
``Ascend`` ``GPU``
Examples:
>>> import numpy as np
>>> import mindspore as ms
>>> from mindspore.explainer.explanation import GradCAM
>>> from mindspore import context
>>>
>>> context.set_context(mode=context.PYNATIVE_MODE)
>>> # The detail of LeNet5 is shown in model_zoo.official.cv.lenet.src.lenet.py
>>> net = LeNet5(10, num_channel=3)
>>> # specify a layer name to generate explanation, usually the layer can be set as the last conv layer.
>>> layer_name = 'conv2'
>>> # init GradCAM with a trained network and specify the layer to obtain attribution
>>> gradcam = GradCAM(net, layer=layer_name)
>>> inputs = ms.Tensor(np.random.rand(1, 3, 32, 32), ms.float32)
>>> label = 5
>>> saliency = gradcam(inputs, label)
>>> print(saliency.shape)
(1, 1, 32, 32)
"""
def __init__(self, network, layer=""):
super(GradCAM, self).__init__(network, layer)
self._saliency_cell = retrieve_layer(self._backward_model, target_layer=layer)
self._avgpool = op.ReduceMean(keep_dims=True)
self._intermediate_grad = None
self._aggregation_fn = _gradcam_aggregation
self._resize_mode = 'bilinear'
def _hook_cell(self):
if self._saliency_cell:
self._saliency_cell.register_backward_hook(self._cell_hook_fn)
self._saliency_cell.enable_hook = True
self._intermediate_grad = None
def _cell_hook_fn(self, _, grad_input, grad_output):
"""
Hook function to deal with the backward gradient.
The arguments are set as required by `Cell.register_backward_hook`.
"""
self._intermediate_grad = grad_input
def __call__(self, inputs, targets):
"""Call function for `GradCAM`."""
self._verify_data(inputs, targets)
self._hook_cell()
with ForwardProbe(self._saliency_cell) as probe:
inputs = unify_inputs(inputs)
targets = unify_targets(targets)
weights = get_bp_weights(self._backward_model, *inputs, targets)
grad_net = GradNet(self._backward_model)
gradients = grad_net(*inputs, weights)
# get intermediate activation
activation = (probe.value,)
if self._layer == "":
activation = inputs
self._intermediate_grad = unify_inputs(gradients)
if self._intermediate_grad is not None:
# average pooling on gradients
intermediate_grad = unify_inputs(
self._avgpool(self._intermediate_grad[0], (2, 3)))
else:
raise ValueError("Gradient for intermediate layer is not "
"obtained")
mul = op.Mul()
attribution = self._aggregation_fn(
mul(*intermediate_grad, *activation))
if self._resize:
attribution = self._resize_fn(attribution, *inputs,
mode=self._resize_mode)
self._intermediate_grad = None
return attribution

View File

@ -1,114 +0,0 @@
# Copyright 2020-2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Gradient explainer."""
from copy import deepcopy
from mindspore.train._utils import check_value_type
from mindspore.explainer._operators import Tensor
from mindspore.explainer._utils import abs_max, deprecated_error, unify_inputs, unify_targets
from .. import Attribution
from .backprop_utils import get_bp_weights, GradNet
@deprecated_error
class Gradient(Attribution):
r"""
Provides Gradient explanation method.
Gradient is the simplest attribution method which uses the naive gradients of outputs w.r.t inputs as the
explanation.
.. math::
attribution = \frac{\partial{y}}{\partial{x}}
Note:
The parsed `network` will be set to eval mode through `network.set_grad(False)` and `network.set_train(False)`.
If you want to train the `network` afterwards, please reset it back to training mode through the opposite
operations.
Args:
network (Cell): The black-box model to be explained.
Inputs:
- **inputs** (Tensor) - The input data to be explained, a 4D tensor of shape :math:`(N, C, H, W)`.
- **targets** (Tensor, int) - The label of interest. It should be a 1D or 0D tensor, or an integer.
If it is a 1D tensor, its length should be the same as `inputs`.
Outputs:
Tensor, a 4D tensor of shape :math:`(N, 1, H, W)`, saliency maps.
Raises:
TypeError: Be raised for any argument type problem.
ValueError: Be raised for any input value problem.
Supported Platforms:
``Ascend`` ``GPU``
Examples:
>>> import numpy as np
>>> import mindspore as ms
>>> from mindspore.explainer.explanation import Gradient
>>> from mindspore import context
>>>
>>> context.set_context(mode=context.PYNATIVE_MODE)
>>> # The detail of LeNet5 is shown in model_zoo.official.cv.lenet.src.lenet.py
>>> net = LeNet5(10, num_channel=3)
>>> gradient = Gradient(net)
>>> inputs = ms.Tensor(np.random.rand(1, 3, 32, 32), ms.float32)
>>> label = 5
>>> saliency = gradient(inputs, label)
>>> print(saliency.shape)
(1, 1, 32, 32)
"""
def __init__(self, network):
super(Gradient, self).__init__(network)
self._backward_model = deepcopy(network)
self._backward_model.set_train(False)
self._backward_model.set_grad(False)
self._grad_net = GradNet(self._backward_model)
self._aggregation_fn = abs_max
def __call__(self, inputs, targets):
"""Call function for `Gradient`."""
self._verify_data(inputs, targets)
inputs = unify_inputs(inputs)
targets = unify_targets(targets)
weights = get_bp_weights(self._backward_model, *inputs, targets)
gradient = self._grad_net(*inputs, weights)
saliency = self._aggregation_fn(gradient)
return saliency
@staticmethod
def _verify_data(inputs, targets):
"""
Verify the validity of the parsed inputs.
Args:
inputs (Tensor): The inputs to be explained.
targets (Tensor, int): The label of interest. It should be a 1D or 0D tensor, or an integer.
If it is a 1D tensor, its length should be the same as `inputs`.
"""
check_value_type('inputs', inputs, Tensor)
if len(inputs.shape) != 4:
raise ValueError(f'Argument inputs must be 4D Tensor. But got {len(inputs.shape)}D Tensor.')
check_value_type('targets', targets, (Tensor, int))
if isinstance(targets, Tensor):
if len(targets.shape) > 1 or (len(targets.shape) == 1 and len(targets) != len(inputs)):
raise ValueError('Argument targets must be a 1D or 0D Tensor. If it is a 1D Tensor, '
'it should have the same length as inputs.')

View File

@ -1,51 +0,0 @@
# Copyright 2020-2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Base class IntermediateLayerAttribution"""
from mindspore.explainer._utils import resize as resize_fn
from .gradient import Gradient
class IntermediateLayerAttribution(Gradient):
"""
Base class for generating _attribution map at intermediate layer.
Args:
network (nn.Cell): DNN model to be explained.
layer (str, optional): string that specifies the layer to generate
intermediate _attribution. When using default value, the input layer
will be specified. Default: ''.
Raises:
TypeError: Be raised for any argument type problem.
"""
def __init__(self, network, layer=''):
super(IntermediateLayerAttribution, self).__init__(network)
# Whether resize the _attribution layer to the input size.
self._resize = True
# string that specifies the resize mode. Default: 'nearest_neighbor'.
self._resize_mode = 'nearest_neighbor'
self._layer = layer
@staticmethod
def _resize_fn(attributions, inputs, mode):
"""Resize the intermediate layer _attribution to the same size as inputs."""
height, width = inputs.shape[2], inputs.shape[3]
return resize_fn(attributions, (height, width), mode)

View File

@ -1,192 +0,0 @@
# Copyright 2020-2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Explainer with modified ReLU."""
import mindspore.nn as nn
import mindspore.ops.operations as op
from mindspore.explainer._utils import (
deprecated_error,
unify_inputs,
unify_targets,
)
from .backprop_utils import GradNet, get_bp_weights
from .gradient import Gradient
class ModifiedReLU(Gradient):
"""Basic class for modified ReLU explanation."""
def __init__(self, network, use_relu_backprop=False):
super(ModifiedReLU, self).__init__(network)
self.use_relu_backprop = use_relu_backprop
self._hook_relu_backward()
self._grad_net = GradNet(self._backward_model)
def __call__(self, inputs, targets):
"""
Call function for `ModifiedReLU`, inherited by "Deconvolution" and "GuidedBackprop".
Args:
inputs (Tensor): The input data to be explained, a 4D tensor of shape :math:`(N, C, H, W)`.
targets (Tensor, int): The label of interest. It should be a 1D or 0D tensor, or an integer.
If it is a 1D tensor, its length should be the same as `inputs`.
Returns:
Tensor, a 4D tensor of shape :math:`(N, 1, H, W)`, saliency maps.
Raises:
TypeError: Be raised for any argument type problem.
ValueError: Be raised for any argument value problem.
Supported Platforms:
``Ascend`` ``GPU``
"""
self._verify_data(inputs, targets)
inputs = unify_inputs(inputs)
targets = unify_targets(targets)
weights = get_bp_weights(self._backward_model, inputs, targets)
gradients = self._grad_net(*inputs, weights)
saliency = self._aggregation_fn(gradients)
return saliency
def _hook_relu_backward(self):
"""Set backward hook for ReLU layers."""
for _, cell in self._backward_model.cells_and_names():
if isinstance(cell, nn.ReLU):
cell.register_backward_hook(self._backward_hook)
def _backward_hook(self, _, grad_inputs, grad_outputs):
"""Hook function for ReLU layers."""
inputs = grad_inputs if self.use_relu_backprop else grad_outputs
relu = op.ReLU()
if isinstance(inputs, tuple):
return relu(*inputs)
return relu(inputs)
@deprecated_error
class Deconvolution(ModifiedReLU):
"""
Deconvolution explanation.
Deconvolution method is a modified version of Gradient method. For the original ReLU operation in the network to be
explained, Deconvolution modifies the propagation rule from directly backpropagating gradients to backprpagating
positive gradients.
Note:
The parsed `network` will be set to eval mode through `network.set_grad(False)` and `network.set_train(False)`.
If you want to train the `network` afterwards, please reset it back to training mode through the opposite
operations. To use `Deconvolution`, the `ReLU` operations in the network must be implemented with
`mindspore.nn.Cell` object rather than `mindspore.ops.Operations.ReLU`. Otherwise, the results will not be
correct.
Args:
network (Cell): The black-box model to be explained.
Inputs:
- **inputs** (Tensor) - The input data to be explained, a 4D tensor of shape :math:`(N, C, H, W)`.
- **targets** (Tensor, int) - The label of interest. It should be a 1D or 0D tensor, or an integer.
If it is a 1D tensor, its length should be the same as `inputs`.
Outputs:
Tensor, a 4D tensor of shape :math:`(N, 1, H, W)`, saliency maps.
Raises:
TypeError: Be raised for any argument or input type problem.
ValueError: Be raised for any input value problem.
Supported Platforms:
``Ascend`` ``GPU``
Examples:
>>> import numpy as np
>>> import mindspore as ms
>>> from mindspore.explainer.explanation import Deconvolution
>>> from mindspore import context
>>>
>>> context.set_context(mode=context.PYNATIVE_MODE)
>>> # The detail of LeNet5 is shown in model_zoo.official.cv.lenet.src.lenet.py
>>> net = LeNet5(10, num_channel=3)
>>> deconvolution = Deconvolution(net)
>>> # parse data and the target label to be explained and get the saliency map
>>> inputs = ms.Tensor(np.random.rand(1, 3, 32, 32), ms.float32)
>>> label = 5
>>> saliency = deconvolution(inputs, label)
>>> print(saliency.shape)
(1, 1, 32, 32)
"""
def __init__(self, network):
super(Deconvolution, self).__init__(network, use_relu_backprop=True)
@deprecated_error
class GuidedBackprop(ModifiedReLU):
"""
Guided-Backpropagation explanation.
Guided-Backpropagation method is an extension of Gradient method. On top of the original ReLU operation in the
network to be explained, Guided-Backpropagation introduces another ReLU operation to filter out the negative
gradients during backpropagation.
Note:
The parsed `network` will be set to eval mode through `network.set_grad(False)` and `network.set_train(False)`.
If you want to train the `network` afterwards, please reset it back to training mode through the opposite
operations. To use `GuidedBackprop`, the `ReLU` operations in the network must be implemented with
`mindspore.nn.Cell` object rather than `mindspore.ops.Operations.ReLU`. Otherwise, the results will not be
correct.
Args:
network (Cell): The black-box model to be explained.
Inputs:
- **inputs** (Tensor) - The input data to be explained, a 4D tensor of shape :math:`(N, C, H, W)`.
- **targets** (Tensor, int) - The label of interest. It should be a 1D or 0D tensor, or an integer.
If it is a 1D tensor, its length should be the same as `inputs`.
Outputs:
Tensor, a 4D tensor of shape :math:`(N, 1, H, W)`, saliency maps.
Raises:
TypeError: Be raised for any argument or input type problem.
ValueError: Be raised for any input value problem.
Supported Platforms:
``Ascend`` ``GPU``
Examples:
>>> import numpy as np
>>> import mindspore as ms
>>> from mindspore.explainer.explanation import GuidedBackprop
>>> from mindspore import context
>>>
>>> context.set_context(mode=context.PYNATIVE_MODE)
>>> # The detail of LeNet5 is shown in model_zoo.official.cv.lenet.src.lenet.py
>>> net = LeNet5(10, num_channel=3)
>>> gbp = GuidedBackprop(net)
>>> # feed data and the target label to be explained and get the saliency map
>>> inputs = ms.Tensor(np.random.rand(1, 3, 32, 32), ms.float32)
>>> label = 5
>>> saliency = gbp(inputs, label)
>>> print(saliency.shape)
(1, 1, 32, 32)
"""
def __init__(self, network):
super(GuidedBackprop, self).__init__(network, use_relu_backprop=False)

View File

@ -1,15 +0,0 @@
# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
""" Perturbation-based _attribution explainer. """

View File

@ -1,200 +0,0 @@
# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Modules to ablate images."""
__all__ = [
'Ablation',
'AblationWithSaliency',
]
import math
from functools import reduce
from typing import Optional, Union
import numpy as np
from .replacement import Constant
from ...._utils import rank_pixels
class Ablation:
"""
Base class to ablate image based on given replacement.
Args:
perturb_mode (str): Perturbation mode.
Inputs:
inputs (np.ndarray): Input array to perturb. The first dim of inputs is assumed to be the batch size, i.e.,
number of samples.
reference (np.ndarray or float): Array of values to replace the elements in the original inputs. The shape
of reference must match the inputs. If scalar is provided, the perturbed elements will be assigned the
given value..
masks (np.ndarray): Several boolean array to mark the perturbed positions. True marks the pixels to be
perturbed, otherwise the pixels will be kept. The shape of masks is assumed to be
[batch_size, num_perturbations, inputs_shape[1:]].
Returns:
np.ndarray, perturbations.
Raises:
TypeError: Be raised for any input type problem.
ValueError: Be raised for any input value problem.
"""
def __init__(self, perturb_mode: str):
self._perturb_mode = perturb_mode
def __call__(self,
inputs: np.array,
reference: Union[np.array, float],
masks: np.array
) -> np.array:
"""Generate perturbations of given array."""
if isinstance(reference, float):
reference = Constant(base_value=reference)(inputs)
if not np.array_equal(inputs.shape, reference.shape):
raise ValueError('reference must have the same shape as inputs.')
num_perturbations = masks.shape[1]
if self._perturb_mode == 'Insertion':
inputs, reference = reference, inputs
perturbations = np.repeat(inputs[:, None, :], num_perturbations, 1)
reference = np.repeat(reference[:, None, :], num_perturbations, 1)
Ablation._assign(perturbations, reference, masks)
return perturbations
@staticmethod
def _assign(original_array: np.ndarray, replacement: np.ndarray, masks: np.ndarray):
"""Assign values to perturb pixels on perturbations."""
if masks.dtype != bool:
raise TypeError('The param "masks" should be an array of bool, but receive {}'.format(masks.dtype))
if not np.array_equal(original_array.shape, masks.shape):
raise ValueError('masks must have the shape {} same as [batch_size, num_perturbations, inputs.shape[1:],'
'but receive {}.'.format(original_array.shape, masks.shape))
original_array[masks] = replacement[masks]
class AblationWithSaliency(Ablation):
"""
Perturbation generator to generate perturbations w.r.t a given saliency map.
Args:
perturb_percent (float): percentage of pixels to perturb
perturb_mode (str): specify perturbing mode, through deleting or
inserting pixels. Current support: ['Deletion', 'Insertion'].
is_accumulate (bool): whether to accumulate the former perturbations to
the later perturbations.
perturb_pixel_per_step (int, optional): number of pixel to perturb
for each perturbation. If perturb_pixel_per_step is None, actual
perturb_pixel_per_step will be calculate by:
num_image_pixel * perturb_percent / num_perturb_steps.
Default: None
num_perturbations (int, optional): number of perturbations. If
num_perturbations if None, it will be calculated by:
num_image_pixel * perturb_percent / perturb_pixel_per_step.
Default: None
"""
def __init__(self,
perturb_mode: str,
perturb_percent: float = 1.0,
is_accumulate: bool = False,
perturb_pixel_per_step: Optional[int] = None,
num_perturbations: Optional[int] = None):
super().__init__(perturb_mode)
self._perturb_percent = perturb_percent
self._perturb_mode = perturb_mode
self._pixel_per_step = perturb_pixel_per_step
self._num_perturbations = num_perturbations
self._is_accumulate = is_accumulate
def generate_mask(self,
saliency: np.ndarray,
num_channels: Optional[int] = None
) -> np.ndarray:
"""
Generate mask for perturbations based on given saliency ranks.
Args:
saliency (numpy.array): Perturbing masks will be generated based on the given saliency map. The shape of
saliency is expected to be: [batch_size, optional(num_channels), *spatial_size]. If multi-channel
saliency is provided, an averaged saliency will be taken to calculate pixel order in spatial dimension.
num_channels (optional[int]): Number of channels of the input data. In order to match the shape of inputs,
num_channels should be provided when input data have channels dimension, even if num_channel is 1.
If None is provided, the inputs is assumed to be no-channel data, and the generated mask will have
no channel dimension. Default: None.
Return:
numpy.array, boolean masks for perturbation generation.
"""
batch_size = saliency.shape[0]
has_channel = num_channels is not None
num_channels = 1 if num_channels is None else num_channels
if has_channel:
saliency = saliency.mean(axis=1)
saliency_rank = rank_pixels(saliency, descending=True)
num_pixels = reduce(lambda x, y: x * y, saliency.shape[1:])
pixel_per_step, num_perturbations = self._check_and_format_perturb_param(num_pixels)
masks = np.zeros((batch_size, num_perturbations, num_channels, saliency_rank.shape[1], saliency_rank.shape[2]),
dtype=np.bool)
# If the perturbation is added accumulately, the factor should be 0 to preserve the low bound of indexing.
factor = 0 if self._is_accumulate else 1
for i in range(batch_size):
low_bound = 0
up_bound = low_bound + pixel_per_step
for j in range(num_perturbations):
masks[i, j, :, ((saliency_rank[i] >= low_bound) & (saliency_rank[i] < up_bound))] = True
low_bound = up_bound * factor
up_bound += pixel_per_step
masks = masks if has_channel else np.squeeze(masks, axis=2)
return masks
def _check_and_format_perturb_param(self, num_pixels):
"""
Check whether the self._pixel_per_step and self._num_perturbation is valid. If the parameters are unreasonable,
this function will try to reassign the parameters and raise ValueError when reassignment is failed.
"""
if self._pixel_per_step:
pixel_per_step = self._pixel_per_step
num_perturbations = math.floor(num_pixels * self._perturb_percent / self._pixel_per_step)
if not num_perturbations:
raise ValueError("Number of perturbations is not valid. Please enlarge the value of perturb_percent or "
"reduce the value of pixel_per_step when instantiating AblationWithSaliency.")
elif self._num_perturbations:
pixel_per_step = math.floor(num_pixels * self._perturb_percent / self._num_perturbations)
num_perturbations = self._num_perturbations
else:
# If neither pixel_per_step or num_perturbations is provided, num_perturbations is determined by the square
# root of product from the spatial size of saliency map.
num_perturbations = math.floor(np.sqrt(num_pixels))
pixel_per_step = math.floor(num_pixels * self._perturb_percent / num_perturbations)
return pixel_per_step, num_perturbations

View File

@ -1,181 +0,0 @@
# Copyright 2020-2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Occlusion explainer."""
from typing import Tuple
import numpy as np
import mindspore as ms
import mindspore.nn as nn
from .ablation import Ablation
from .perturbation import PerturbationAttribution
from .replacement import Constant
from ...._utils import abs_max, deprecated_error
def _generate_patches(array, window_size: Tuple, strides: Tuple):
"""Generate patches from image w.r.t given window_size and strides."""
window_strides = array.strides
slices = tuple(slice(None, None, stride) for stride in strides)
indexing_strides = array[slices].strides
win_indices_shape = (np.array(array.shape) - np.array(window_size)) // np.array(strides) + 1
patches_shape = tuple(win_indices_shape) + window_size
strides_in_memory = indexing_strides + window_strides
patches = np.lib.stride_tricks.as_strided(array, shape=patches_shape, strides=strides_in_memory, writeable=False)
patches = patches.reshape((-1,) + window_size)
return patches
@deprecated_error
class Occlusion(PerturbationAttribution):
"""
Occlusion uses a sliding window to replace the pixels with a reference value (e.g. constant value), and computes
the output difference w.r.t the original output. The output difference caused by perturbed pixels are assigned as
feature importance to those pixels. For pixels involved in multiple sliding windows, the feature importance is the
averaged differences from multiple sliding windows.
For more details, please refer to the original paper via: `<https://arxiv.org/abs/1311.2901>`_.
Args:
network (Cell): The black-box model to be explained.
activation_fn (Cell): The activation layer that transforms logits to prediction probabilities. For
single label classification tasks, `nn.Softmax` is usually applied. As for multi-label classification
tasks,`nn.Sigmoid` is usually be applied. Users can also pass their own customized `activation_fn` as long
as when combining this function with network, the final output is the probability of the input.
perturbation_per_eval (int, optional): Number of perturbations for each inference during inferring the
perturbed samples. Within the memory capacity, usually the larger this number is, the faster the
explanation is obtained. Default: 32.
Inputs:
- **inputs** (Tensor) - The input data to be explained, a 4D tensor of shape :math:`(N, C, H, W)`.
- **targets** (Tensor, int) - The label of interest. It should be a 1D or 0D tensor, or an integer.
If it is a 1D tensor, its length should be the same as `inputs`.
Outputs:
Tensor, a 4D tensor of shape :math:`(N, 1, H, W)`, saliency maps.
Raises:
TypeError: Be raised for any argument or input type problem.
ValueError: Be raised for any input value problem.
Supported Platforms:
``Ascend`` ``GPU``
Example:
>>> import numpy as np
>>> import mindspore as ms
>>> from mindspore.explainer.explanation import Occlusion
>>> from mindspore import context
>>>
>>> context.set_context(mode=context.PYNATIVE_MODE)
>>> # The detail of LeNet5 is shown in model_zoo.official.cv.lenet.src.lenet.py
>>> net = LeNet5(10, num_channel=3)
>>> # initialize Occlusion explainer with the pretrained model and activation function
>>> activation_fn = ms.nn.Softmax() # softmax layer is applied to transform logits to probabilities
>>> occlusion = Occlusion(net, activation_fn=activation_fn)
>>> input_x = ms.Tensor(np.random.rand(1, 3, 32, 32), ms.float32)
>>> label = ms.Tensor([1], ms.int32)
>>> saliency = occlusion(input_x, label)
>>> print(saliency.shape)
(1, 1, 32, 32)
"""
def __init__(self, network, activation_fn, perturbation_per_eval=32):
super().__init__(network, activation_fn, perturbation_per_eval)
self._ablation = Ablation(perturb_mode='Deletion')
self._aggregation_fn = abs_max
self._get_replacement = Constant(base_value=0.0)
self._num_sample_per_dim = 32 # specify the number of perturbations each dimension.
def __call__(self, inputs, targets):
"""Call function for 'Occlusion'."""
self._verify_data(inputs, targets)
inputs = inputs.asnumpy()
targets = targets.asnumpy() if isinstance(targets, ms.Tensor) else np.array([targets], np.int)
batch_size = inputs.shape[0]
window_size, strides = self._get_window_size_and_strides(inputs)
full_network = nn.SequentialCell([self._network, self._activation_fn])
original_outputs = full_network(ms.Tensor(inputs, ms.float32)).asnumpy()[np.arange(batch_size), targets]
masks = Occlusion._generate_masks(inputs, window_size, strides)
return self._perturbate(batch_size, full_network, (original_outputs, masks, inputs, targets))
def _perturbate(self, batch_size, full_network, data):
"""Perform perturbations."""
original_outputs, masks, inputs, targets = data
total_attribution = np.zeros_like(inputs)
weights = np.ones_like(inputs)
num_perturbations = masks.shape[1]
reference = self._get_replacement(inputs)
count = 0
while count < num_perturbations:
ith_masks = masks[:, count:min(count+self._perturbation_per_eval, num_perturbations)]
actual_num_eval = ith_masks.shape[1]
num_samples = batch_size * actual_num_eval
occluded_inputs = self._ablation(inputs, reference, ith_masks)
occluded_inputs = occluded_inputs.reshape((-1, *inputs.shape[1:]))
targets_repeat = np.repeat(targets, repeats=actual_num_eval, axis=0)
occluded_outputs = full_network(
ms.Tensor(occluded_inputs, ms.float32)).asnumpy()[np.arange(num_samples), targets_repeat]
original_outputs_repeat = np.repeat(original_outputs, repeats=actual_num_eval, axis=0)
outputs_diff = original_outputs_repeat - occluded_outputs
total_attribution += (
outputs_diff.reshape(ith_masks.shape[:2] + (1,) * (len(masks.shape) - 2)) * ith_masks).sum(axis=1)
weights += ith_masks.sum(axis=1)
count += actual_num_eval
attribution = self._aggregation_fn(ms.Tensor(total_attribution / weights, ms.float32))
return attribution
def _get_window_size_and_strides(self, inputs):
"""
Return window_size and strides.
# If spatial size of input data is smaller than self._num_sample_per_dim, window_size and strides will set to
# `(C, 3, 3)` and `(C, 1, 1)` separately. Otherwise, the window_size and strides will generated adaptively to
match self._num_sample_per_dim.
"""
window_size = tuple(
[inputs.shape[1]]
+ [x // self._num_sample_per_dim if x > self._num_sample_per_dim else 3 for x in inputs.shape[2:]])
strides = tuple(
[inputs.shape[1]]
+ [x // self._num_sample_per_dim if x > self._num_sample_per_dim else 1 for x in inputs.shape[2:]])
return window_size, strides
@staticmethod
def _generate_masks(inputs, window_size, strides):
"""Generate masks to perturb contiguous regions."""
total_dim = np.prod(inputs.shape[1:]).item()
template = np.arange(total_dim).reshape(inputs.shape[1:])
indices = _generate_patches(template, window_size, strides)
num_perturbations = indices.shape[0]
indices = indices.reshape(num_perturbations, -1)
mask = np.zeros((num_perturbations, total_dim), dtype=np.bool)
for i in range(num_perturbations):
mask[i, indices[i]] = True
mask = mask.reshape((num_perturbations,) + inputs.shape[1:])
masks = np.tile(mask, reps=(inputs.shape[0],) + (1,) * len(mask.shape))
return masks

View File

@ -1,42 +0,0 @@
# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Base class `PerturbationAttribtuion`"""
from mindspore.train._utils import check_value_type
from mindspore.nn import Cell
from ..attribution import Attribution
class PerturbationAttribution(Attribution):
"""
Base class for perturbation-based attribution methods.
All perturbation-based _attribution methods extend from this class.
"""
def __init__(self,
network,
activation_fn,
perturbation_per_eval,
):
super(PerturbationAttribution, self).__init__(network)
check_value_type("activation_fn", activation_fn, Cell)
self._activation_fn = activation_fn
check_value_type('perturbation_per_eval', perturbation_per_eval, int)
if perturbation_per_eval <= 0:
raise ValueError('Argument perturbation_per_eval should be a positive integer.')
self._perturbation_per_eval = perturbation_per_eval

View File

@ -1,85 +0,0 @@
# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Modules to generate perturbations."""
import numpy as np
from scipy.ndimage.filters import gaussian_filter
_Array = np.ndarray
__all__ = [
'BaseReplacement',
'Constant',
'GaussianBlur',
'RandomPerturb',
]
class BaseReplacement:
"""
Base class of generator for generating different replacement for perturbations.
Args:
kwargs: Optional args for generating replacement. Derived class need to
add necessary arg names and default value to '_necessary_args'.
If the argument has no default value, the value should be set to
'EMPTY' to mark the required args. Initializing an object will
check the given kwargs w.r.t '_necessary_args'.
Raises:
ValueError: Raise when provided kwargs not contain necessary arg names with 'EMPTY' mark.
"""
_necessary_args = {}
def __init__(self, **kwargs):
self._replace_args = self._necessary_args.copy()
for key, value in self._replace_args.items():
if key in kwargs.keys():
self._replace_args[key] = kwargs[key]
elif key not in kwargs.keys() and value == 'EMPTY':
raise ValueError(f"Missing keyword arg {key} for {self.__class__.__name__}.")
def __call__(self, inputs):
raise NotImplementedError()
class Constant(BaseReplacement):
"""Generator to provide constant-value replacement for perturbations."""
_necessary_args = {'base_value': 'EMPTY'}
def __call__(self, inputs: _Array) -> _Array:
replacement = np.ones_like(inputs, dtype=np.float32)
replacement *= self._replace_args['base_value']
return replacement
class GaussianBlur(BaseReplacement):
"""Generator to provided gaussian blurred inputs for perturbation"""
_necessary_args = {'sigma': 0.7}
def __call__(self, inputs: _Array) -> _Array:
sigma = self._replace_args['sigma']
replacement = gaussian_filter(inputs, sigma=sigma)
return replacement
class RandomPerturb(BaseReplacement):
"""Generator to provide replacement by randomly adding noise."""
_necessary_args = {'radius': 0.2}
def __call__(self, inputs: _Array) -> _Array:
radius = self._replace_args['radius']
outputs = inputs + (2 * np.random.rand(*inputs.shape) - 1) * radius
return outputs

View File

@ -1,194 +0,0 @@
# Copyright 2020-2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""RISE."""
import math
import numpy as np
from mindspore import Tensor
from mindspore.train._utils import check_value_type
from .perturbation import PerturbationAttribution
from .... import _operators as op
from ...._utils import resize, deprecated_error
@deprecated_error
class RISE(PerturbationAttribution):
r"""
RISE: Randomized Input Sampling for Explanation of Black-box Model.
RISE is a perturbation-based method that generates attribution maps by sampling on multiple random binary masks.
The original image is randomly masked, and then fed into the black-box model to get predictions. The final
attribution map is the weighted sum of these random masks, with the weights being the corresponding output on the
node of interest:
.. math::
attribution = \sum_{i}f_c(I\odot M_i) M_i
For more details, please refer to the original paper via: `RISE <https://arxiv.org/abs/1806.07421>`_.
Args:
network (Cell): The black-box model to be explained.
activation_fn (Cell): The activation layer that transforms logits to prediction probabilities. For
single label classification tasks, `nn.Softmax` is usually applied. As for multi-label classification
tasks, `nn.Sigmoid` is usually be applied. Users can also pass their own customized `activation_fn` as long
as when combining this function with network, the final output is the probability of the input.
perturbation_per_eval (int, optional): Number of perturbations for each inference during inferring the
perturbed samples. Within the memory capacity, usually the larger this number is, the faster the
explanation is obtained. Default: 32.
Inputs:
- **inputs** (Tensor) - The input data to be explained, a 4D tensor of shape :math:`(N, C, H, W)`.
- **targets** (Tensor, int) - The labels of interest to be explained. When `targets` is an integer,
all of the inputs will generates attribution map w.r.t this integer. When `targets` is a tensor, it
should be of shape :math:`(N, l)` (l being the number of labels for each sample) or :math:`(N,)` :math:`()`.
Outputs:
Tensor, a 4D tensor of shape :math:`(N, l, H, W)` when targets is a tensor of shape (N, l), otherwise a tensor
of shape (N, 1, H, w), saliency maps.
Raises:
TypeError: Be raised for any argument or input type problem.
ValueError: Be raised for any input value problem.
Supported Platforms:
``Ascend`` ``GPU``
Examples:
>>> import numpy as np
>>> import mindspore as ms
>>> from mindspore.explainer.explanation import RISE
>>> from mindspore import context
>>>
>>> context.set_context(mode=context.PYNATIVE_MODE)
>>> # The detail of LeNet5 is shown in model_zoo.official.cv.lenet.src.lenet.py
>>> net = LeNet5(10, num_channel=3)
>>> # initialize RISE explainer with the pretrained model and activation function
>>> activation_fn = ms.nn.Softmax() # softmax layer is applied to transform logits to probabilities
>>> rise = RISE(net, activation_fn=activation_fn)
>>> # given an instance of RISE, saliency map can be generate
>>> inputs = ms.Tensor(np.random.rand(2, 3, 32, 32), ms.float32)
>>> # when `targets` is an integer
>>> targets = 5
>>> saliency = rise(inputs, targets)
>>> print(saliency.shape)
(2, 1, 32, 32)
>>> # `targets` can also be a 2D tensor
>>> targets = ms.Tensor([[5], [1]], ms.int32)
>>> saliency = rise(inputs, targets)
>>> print(saliency.shape)
(2, 1, 32, 32)
"""
def __init__(self,
network,
activation_fn,
perturbation_per_eval=32):
super(RISE, self).__init__(network, activation_fn, perturbation_per_eval)
self._num_masks = 6000 # number of masks to be sampled
self._mask_probability = 0.5 # ratio of inputs to be masked
self._down_sample_size = 10 # the original size of binary masks
self._resize_mode = 'bilinear' # mode choice to resize the down-sized binary masks to size of the inputs
self._perturbation_mode = 'constant' # setting the perturbed pixels to a constant value
self._base_value = 0 # setting the perturbed pixels to this constant value
self._num_classes = None # placeholder of self._num_classes just for future assignment in other methods
def _generate_masks(self, data, batch_size):
"""Generate a batch of binary masks for data."""
height, width = data.shape[2], data.shape[3]
mask_size = (self._down_sample_size, self._down_sample_size)
up_size = (height + mask_size[0], width + mask_size[1])
mask = np.random.random((batch_size, 1) + mask_size) < self._mask_probability
upsample = resize(op.Tensor(mask, data.dtype), up_size,
self._resize_mode).asnumpy()
shift_x = np.random.randint(0, mask_size[0] + 1, size=batch_size)
shift_y = np.random.randint(0, mask_size[1] + 1, size=batch_size)
masks = [sample[:, x_i: x_i + height, y_i: y_i + width] for sample, x_i, y_i
in zip(upsample, shift_x, shift_y)]
masks = Tensor(np.array(masks), data.dtype)
return masks
def __call__(self, inputs, targets):
"""Generates attribution maps for inputs."""
self._verify_data(inputs, targets)
height, width = inputs.shape[2], inputs.shape[3]
if self._num_classes is None:
self._num_classes = self.network(inputs).shape[1]
# Due to the unsupported Op of slice assignment, we use numpy array here
targets = self._unify_targets(inputs, targets)
attr_np = np.zeros(shape=(inputs.shape[0], targets.shape[1], height, width))
cal_times = math.ceil(self._num_masks / self._perturbation_per_eval)
for idx, data in enumerate(inputs):
bg_data = data * 0 + self._base_value
data = op.reshape(data, (1, -1, height, width))
for j in range(cal_times):
bs = min(self._num_masks - j * self._perturbation_per_eval,
self._perturbation_per_eval)
masks = self._generate_masks(data, bs)
weights = masks * data + (1 - masks) * bg_data
weights = self._activation_fn(self.network(weights))
while len(weights.shape) > 2:
weights = op.mean(weights, axis=2)
weights = np.expand_dims(np.expand_dims(weights.asnumpy()[:, targets[idx]], 2), 3)
attr_np[idx] += np.sum(weights * masks.asnumpy(), axis=0)
attr_np = attr_np / self._num_masks
return op.Tensor(attr_np, dtype=inputs.dtype)
@staticmethod
def _verify_data(inputs, targets):
"""Verify the validity of the parsed inputs."""
check_value_type('inputs', inputs, Tensor)
if len(inputs.shape) != 4:
raise ValueError(f'Argument inputs must be 4D Tensor, but got {len(inputs.shape)}D Tensor.')
check_value_type('targets', targets, (Tensor, int, tuple, list))
if isinstance(targets, Tensor):
if len(targets.shape) > 2:
raise ValueError('Dimension invalid. If `targets` is a Tensor, it should be 0D, 1D or 2D. '
'But got {}D.'.format(len(targets.shape)))
if targets.shape and len(targets) != len(inputs):
raise ValueError(
'If `targets` is a 2D, 1D Tensor, it should have the same length as inputs {}. But got {}.'.format(
len(inputs), len(targets)))
@staticmethod
def _unify_targets(inputs, targets):
"""To unify targets to be 2D numpy.ndarray."""
if isinstance(targets, int):
return np.array([[targets] for _ in inputs]).astype(np.int)
if isinstance(targets, Tensor):
if not targets.shape:
return np.array([[targets.asnumpy()] for _ in inputs]).astype(np.int)
if len(targets.shape) == 1:
return np.array([[t.asnumpy()] for t in targets]).astype(np.int)
if len(targets.shape) == 2:
return np.array([t.asnumpy() for t in targets]).astype(np.int)
return targets

View File

@ -1,77 +0,0 @@
# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Attribution."""
from typing import Callable
import mindspore as ms
import mindspore.nn as nn
from mindspore.train._utils import check_value_type
class Attribution:
"""
Basic class of attributing the salient score
The explainers which explanation through attributing the relevance scores should inherit this class.
Args:
network (nn.Cell): The black-box model to be explained.
"""
def __init__(self, network):
check_value_type("network", network, nn.Cell)
self._network = network
self._network.set_train(False)
self._network.set_grad(False)
@staticmethod
def _verify_network(network):
"""Verify the input `network` for __init__ function."""
if not isinstance(network, nn.Cell):
raise TypeError("The parsed `network` must be a `mindspore.nn.Cell` object.")
__call__: Callable
"""
The explainers return the explanations by calling directly on the explanation.
Derived class should overwrite this implementations for different
algorithms.
Args:
input (ms.Tensor): Input tensor to be explained.
Returns:
- saliency map (ms.Tensor): saliency map of the input.
"""
@property
def network(self):
"""Return the model."""
return self._network
@staticmethod
def _verify_data(inputs, targets):
"""Verify the validity of the parsed inputs."""
check_value_type('inputs', inputs, ms.Tensor)
if len(inputs.shape) != 4:
raise ValueError('Argument inputs must be 4D Tensor')
check_value_type('targets', targets, (ms.Tensor, int))
if isinstance(targets, ms.Tensor):
if len(targets.shape) > 1 or (len(targets.shape) == 1 and len(targets) != len(inputs)):
raise ValueError('Argument targets must be a 1D or 0D Tensor. If it is a 1D Tensor, '
'it should have the same length as inputs.')
elif inputs.shape[0] != 1:
raise ValueError('If targets have type of int, batch_size of inputs should equals 1. Receive batch_size {}'
.format(inputs.shape[0]))

View File

@ -1,15 +0,0 @@
# Copyright 2021 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Counterfactual modules."""

View File

@ -1,48 +0,0 @@
# Copyright 2020 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Generate the explain event which conform to proto format."""
import time
from ..summary_pb2 import Event, Explain
def check_explain_proto(explain):
"""
Package the explain event.
Args:
explain (Explain): The object of summary_pb2.Explain.
"""
if not isinstance(explain, Explain):
raise TypeError(f'Plugin explainer expects a {Explain.__name__} value.')
if not explain.image_path and not explain.inference and not explain.metadata.label and not explain.benchmark:
raise ValueError('One of metadata, image path, inference or benchmark has to be filled in.')
def package_explain_event(explain_str):
"""
Package the explain event.
Args:
explain_str (string): The serialize string of summary_pb2.Explain.
Returns:
Event, event object.
"""
event = Event()
event.wall_time = time.time()
event.explain.ParseFromString(explain_str)
return event.SerializeToString()

View File

@ -26,8 +26,7 @@ from mindspore.train.summary.enums import PluginEnum, WriterPluginEnum
from ._lineage_adapter import serialize_to_lineage_event
from ._summary_adapter import package_graph_event, package_summary_event
from ._explain_adapter import package_explain_event
from .writer import LineageWriter, SummaryWriter, ExplainWriter, ExportWriter
from .writer import LineageWriter, SummaryWriter, ExportWriter
try:
from multiprocessing import get_context
@ -50,8 +49,6 @@ def _pack_data(datadict, wall_time):
PluginEnum.IMAGE.value):
summaries.append({'_type': plugin.title(), 'name': data.get('tag'), 'data': data.get('value')})
step = data.get('step')
elif plugin == PluginEnum.EXPLAINER.value:
result.append([plugin, package_explain_event(data.get('value'))])
if 'export_option' in data:
result.append([WriterPluginEnum.EXPORTER.value, data])
@ -85,6 +82,7 @@ class WriterPool(ctx.Process):
self.start()
def run(self):
"""Run the writer pool."""
# Environment variables are used to specify a maximum number of OpenBLAS threads:
# In ubuntu(GPU) environment, numpy will use too many threads for computing,
# it may affect the start of the summary process.
@ -138,8 +136,6 @@ class WriterPool(ctx.Process):
self._writers_.append(SummaryWriter(filepath, self._max_file_size))
elif plugin == WriterPluginEnum.LINEAGE.value:
self._writers_.append(LineageWriter(filepath, self._max_file_size))
elif plugin == WriterPluginEnum.EXPLAINER.value:
self._writers_.append(ExplainWriter(filepath, self._max_file_size))
elif plugin == WriterPluginEnum.EXPORTER.value:
self._writers_.append(ExportWriter(filepath, self._max_file_size))
return self._writers_

View File

@ -27,7 +27,6 @@ from ..._c_expression import Tensor, security
from ..._checkparam import Validator
from .._utils import _check_lineage_value, _check_to_numpy, _make_directory, check_value_type
from ._summary_adapter import get_event_file_name, package_graph_event
from ._explain_adapter import check_explain_proto
from ._writer_pool import WriterPool
# for the moment, this lock is for caution's sake,
@ -190,7 +189,6 @@ class SummaryRecord:
filename_dict = dict(summary=self.file_info.get('file_name'),
lineage=get_event_file_name(file_prefix, '_lineage', time_second),
explainer=get_event_file_name(file_prefix, '_explain', time_second),
exporter=export_dir)
self._event_writer = WriterPool(log_dir,
max_file_size,
@ -253,8 +251,6 @@ class SummaryRecord:
see mindspore/ccsrc/lineage.proto.
- The data type of value should be a 'UserDefinedInfo' object when the plugin is 'custom_lineage_data',
see mindspore/ccsrc/lineage.proto.
- The data type of value should be a 'Explain' object when the plugin is 'explainer',
see mindspore/ccsrc/summary.proto.
Raises:
ValueError: If the parameter value is invalid.
TypeError: If the parameter type is error.
@ -287,9 +283,6 @@ class SummaryRecord:
elif plugin == 'graph':
package_graph_event(value)
self._data_pool[plugin].append(dict(value=value))
elif plugin == 'explainer':
check_explain_proto(value)
self._data_pool[plugin].append(dict(value=value.SerializeToString()))
else:
raise ValueError(f'No such plugin of {repr(plugin)}')

View File

@ -112,15 +112,6 @@ class LineageWriter(BaseWriter):
super().write(plugin, data)
class ExplainWriter(BaseWriter):
"""ExplainWriter for write explain data."""
def write(self, plugin, data):
"""Write data to file."""
if plugin == WriterPluginEnum.EXPLAINER.value:
super().write(plugin, data)
class ExportWriter(BaseWriter):
"""ExportWriter for export data."""