forked from mindspore-Ecosystem/mindspore
!26098 Remove explainer code
Merge pull request !26098 from TonyNG/remove_explainer
This commit is contained in:
commit
48c7b73fd1
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
approvers:
|
||||
- ouwenchang
|
||||
- wangyue01
|
||||
- wenkai_dist
|
||||
- lilongfei15
|
||||
- lixiaohui33
|
|
@ -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
|
@ -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)
|
|
@ -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
|
|
@ -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"
|
||||
]
|
|
@ -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"""
|
|
@ -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)
|
|
@ -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))
|
|
@ -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.")
|
|
@ -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)))
|
|
@ -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
|
|
@ -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',
|
||||
]
|
|
@ -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'
|
||||
]
|
|
@ -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."""
|
|
@ -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
|
|
@ -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
|
|
@ -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.')
|
|
@ -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)
|
|
@ -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)
|
|
@ -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. """
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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]))
|
|
@ -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."""
|
File diff suppressed because it is too large
Load Diff
|
@ -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()
|
|
@ -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_
|
||||
|
|
|
@ -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)}')
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
Loading…
Reference in New Issue