# Copyright 2022 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.
# ============================================================================
"""Shap gradient explainer."""
import numpy as np
import mindspore as ms
from mindspore.train._utils import check_value_type
from mindspore_xai.common.utils import is_notebook
from mindspore_xai.third_party.shap.shap import GradientExplainer
from .shap import _SHAP
[文档]class SHAPGradient(_SHAP):
r"""
Provides SHAP gradient explanation method.
Explains a network using expected gradients (an extension of integrated 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.
Args:
network (Cell): The mindspore cell to be explained. For classification, it accepts a 2D tensor of
shape :math:`(N, K)` as input and outputs a 2D tensor of shape :math:`(N, L)`. For regression, it
accepts a 2D tensor of shape :math:`(N, K)` as input and outputs a 1D tensor of
shape :math:`(N)`.
features (Tensor): 2D tensor of shape :math:`(N, K)` (N being the number of samples, K being the number of
features). The background dataset to use for integrating out features, accept (whole or part of) training
dataset.
feature_names (list, optional): list of names (strings) corresponding to the columns in the training data.
Default: `None`.
class_names (list, optional): list of class names, ordered according to whatever the classifier is using. If
not present, class names will be '0', '1', ... Default: `None`.
num_neighbours (int, optional): Number of subsets used for the estimation of the shap values. Default: 200.
max_features (int, optional): Maximum number of features present in explanation. Default: 10.
Inputs:
- **inputs** (Tensor) - The input data to be explained, a 2D float tensor of shape :math:`(N, K)`.
- **targets** (Tensor, numpy.ndarray, list, int, optional) - The labels of interest to be explained. When
`targets` is an integer, all the inputs will generate attribution map w.r.t this integer. When `targets` is a
tensor or numpy array or list, it should be of shape :math:`(N, L)` (L being the number of labels for each
sample), :math:`(N,)` or :math:`()`. Default: 0.
- **show** (bool, optional): Show the explanation figures, `None` means automatically show the explanation
figures if it is running on JupyterLab. Default: `None`.
Outputs:
Tensor, a 3D tensor of shape :math:`(N, L, K)`. The first dimension represents inputs.
The second dimension represents targets. The third dimension represents feature weight.
Supported Platforms:
``Ascend`` ``GPU``
Examples:
>>> import numpy as np
>>> import mindspore as ms
>>> import mindspore.nn as nn
>>> from mindspore import set_context, PYNATIVE_MODE
>>> from mindspore_xai.explainer import SHAPGradient
>>>
>>> set_context(mode=PYNATIVE_MODE)
>>> # Linear classification model
>>> class LinearNet(nn.Cell):
... def __init__(self, num_inputs, num_class):
... super(LinearNet, self).__init__()
... self.fc = nn.Dense(num_inputs, num_class, activation=nn.Softmax())
... def construct(self, x):
... x = self.fc(x)
... return x
>>> net = LinearNet(4, 3)
>>> # use iris data as example
>>> feature_names = ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
>>> class_names = ['setosa', 'versicolor', 'virginica']
>>> training_data = ms.Tensor(np.random.rand(10, 4), ms.float32)
>>> shap = SHAPGradient(net, training_data, feature_names=feature_names, class_names=class_names)
>>> inputs = ms.Tensor(np.random.rand(2, 4), ms.float32)
>>> targets = ms.Tensor([[1, 2], [1, 2]], ms.int32)
>>> exps = shap(inputs, targets)
>>> print(exps.shape)
(2, 2, 4)
"""
def __init__(self, network, features, feature_names=None, class_names=None, num_neighbours=200, max_features=10):
check_value_type("network", network, ms.nn.Cell)
check_value_type("features", features, ms.Tensor)
check_value_type("num_neighbours", num_neighbours, int)
check_value_type("max_features", max_features, int)
check_value_type("feature_names", feature_names, [list, type(None)])
check_value_type("class_names", class_names, [list, type(None)])
super().__init__(network, features, feature_names, class_names)
self._impl = GradientExplainer(network, features, self._mode, self._predictor_num_outputs)
self._num_neighbours = num_neighbours
self._max_features = max_features
def __call__(self, inputs, targets=0, show=None):
check_value_type("inputs", inputs, ms.Tensor)
check_value_type("targets", targets, [ms.Tensor, np.ndarray, list, int])
check_value_type("show", show, [bool, type(None)])
if len(inputs.shape) != 2:
raise ValueError('Dimension invalid. `inputs` should be 2D. '
'But got {}D.'.format(len(inputs.shape)))
targets = self._unify_targets(inputs, targets)
exps = self._impl.shap_values(inputs, targets, self._num_neighbours)
if show is None:
show = is_notebook()
if show:
self._show_all(exps, targets, self._impl.expected_value, self._max_features)
return exps