forked from huawei/mindspore2022
178 lines
7.8 KiB
Python
178 lines
7.8 KiB
Python
# 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 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
|
|
|
|
|
|
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.")
|