import numpy as np
from tqdm import tqdm
from .abc_interpreter import InputGradientInterpreter
from ..data_processor.readers import images_transform_pipeline, preprocess_save_path
from ..data_processor.visualizer import explanation_to_vis, show_vis_explanation, save_image
[docs]class SmoothGradInterpreterV2(InputGradientInterpreter):
"""
Smooth Gradients Interpreter.
For input gradient based interpreters, the target issue is generally the vanilla input gradient's noises.
The basic idea of reducing the noises is to use different similar inputs to get the input gradients and
do the average.
Smooth Gradients method solves the problem of meaningless local variations in partial derivatives
by adding random noise to the inputs multiple times and take the average of the gradients.
This ``SmoothGradInterpreterV2`` only optimizes the GPU usage issue where large GPU memory usage may cause
an error for large models and large batch sizes.
More details regarding the Smooth Gradients method can be found in the original paper:
http://arxiv.org/abs/1706.03825.
"""
def __init__(self, model: callable, device: str = 'gpu:0'):
"""
Args:
model (callable): A model with :py:func:`forward` and possibly :py:func:`backward` functions.
device (str): The device used for running ``model``, options: ``"cpu"``, ``"gpu:0"``, ``"gpu:1"``
etc.
"""
InputGradientInterpreter.__init__(self, model, device)
[docs] def interpret(self,
inputs: str or list(str) or np.ndarray,
labels: list or np.ndarray = None,
noise_amount: int = 0.1,
n_samples: int = 50,
split: int = 2,
gradient_of: str = 'probability',
resize_to: int = 224,
crop_to: int = None,
visual: bool = True,
save_path: str = None) -> np.ndarray:
"""The technical details of the SmoothGrad method are described as follows:
SmoothGrad generates ``n_samples`` noised inputs, with the noise scale of ``noise_amount``, and then computes
the gradients *w.r.t.* these noised inputs. The final explanation is averaged gradients. The difference to
:class:`SmoothGradInterpreter` is an additional argument ``split``, where total samples are divided into
``split`` parts to pass the model, to avoid large GPU memory usages.
Args:
inputs (str or list): The input image filepath or a list of filepaths or numpy array of read images.
labels (list or np.ndarray, optional): The target labels to analyze. The number of labels should be equal
to the number of images. If None, the most likely label for each image will be used. Default: ``None``.
noise_amount (int, optional): Noise level of added noise to the image. The std of Gaussian random noise
is ``noise_amount`` * (x :sub:`max` - x :sub:`min`). Default: ``0.1``.
n_samples (int, optional): The number of new images generated by adding noise. Default: ``50``.
split (int, optional): The number of splits. Default: ``2``.
gradient_of (str, optional): compute the gradient of ['probability', 'logit' or 'loss']. Default:
``'probability'``. SmoothGrad uses probability for all tasks by default.
resize_to (int, optional): Images will be rescaled with the shorter edge being ``resize_to``. Defaults to
``224``.
crop_to (int, optional): After resize, images will be center cropped to a square image with the size
``crop_to``. If None, no crop will be performed. Defaults to ``None``.
visual (bool, optional): Whether or not to visualize the processed image. Default: ``True``.
save_path (str, optional): The filepath(s) to save the processed image(s). If None, the image will not be
saved. Default: ``None``.
Returns:
np.ndarray: the explanation result.
"""
imgs, data = images_transform_pipeline(inputs, resize_to, crop_to)
# print(imgs.shape, data.shape, imgs.dtype, data.dtype) # (1, 224, 224, 3) (1, 3, 224, 224) uint8 float32
assert len(data) == 1, "interpret each sample individually, it is optimized."
self._build_predict_fn(gradient_of=gradient_of)
# obtain the labels (and initialization).
_, predicted_label, predicted_proba = self.predict_fn(data, labels)
self.predicted_label = predicted_label
self.predicted_proba = predicted_proba
if labels is None:
labels = predicted_label
labels = np.array(labels).reshape((1, ))
# SmoothGrad
max_axis = tuple(np.arange(1, data.ndim))
stds = noise_amount * (np.max(data, axis=max_axis) - np.min(data, axis=max_axis))
data_noised = []
for i in range(n_samples):
noise = np.concatenate(
[np.float32(np.random.normal(0.0, stds[j], (1, ) + tuple(d.shape))) for j, d in enumerate(data)])
data_noised.append(data + noise)
data_noised = np.concatenate(data_noised, axis=0)
# print(data_i.shape, labels.shape)
# print(data_noised.shape) # n_samples, 3, 224, 224
# splits, to avoid large GPU memory usage.
if split > 1:
chunk = n_samples // split
gradient_chunks = []
for i in range(split - 1):
gradients_i, _, _ = self.predict_fn(data_noised[i * chunk:(i + 1) * chunk], np.repeat(labels, chunk))
gradient_chunks.append(gradients_i)
gradients_s, _, _ = self.predict_fn(data_noised[chunk * (split - 1):],
np.repeat(labels, n_samples - chunk * (split - 1)))
gradient_chunks.append(gradients_s)
gradients = np.concatenate(gradient_chunks, axis=0)
else:
# one split.
gradients, _, _ = self.predict_fn(data_noised, np.repeat(labels, n_samples))
avg_gradients = np.mean(gradients, axis=0, keepdims=True)
# visualize and save image.
if save_path is None and not visual:
# no need to visualize or save explanation results.
pass
else:
save_path = preprocess_save_path(save_path, 1)
# print(imgs[i].shape, avg_gradients[i].shape)
vis_explanation = explanation_to_vis(imgs[i], np.abs(avg_gradients[0]).sum(0), style='overlay_grayscale')
if visual:
show_vis_explanation(vis_explanation)
if save_path[i] is not None:
save_image(save_path[i], vis_explanation)
# intermediate results, for possible further usages.
self.labels = labels
return avg_gradients